From 6b011af032efa80a0b0f6a8f66189cca907e497f Mon Sep 17 00:00:00 2001 From: Looly Date: Wed, 14 Aug 2019 10:02:32 +0800 Subject: [PATCH] clean history --- .gitee/ISSUE_TEMPLATE.zh-CN.md | 5 + .gitee/PULL_REQUEST_TEMPLATE.zh-CN.md | 10 + .gitignore | 32 + .travis.yml | 24 + CHANGELOG.md | 1316 ++++++ LICENSE.txt | 121 + README.md | 175 + bin/check_dependency_updates.sh | 7 + bin/commit.sh | 6 + bin/deploy.sh | 3 + bin/install.sh | 3 + bin/javadoc.sh | 3 + bin/logo.sh | 11 + bin/push_dev.sh | 9 + bin/push_master.sh | 12 + bin/replaceVersion.sh | 34 + bin/test.sh | 3 + bin/update_version.sh | 21 + bin/version.txt | 1 + docs/.nojekyll | 0 docs/docs/index.html | 74 + docs/index.html | 497 ++ docs/js/version.js | 1 + hutool-all/pom.xml | 140 + .../src/main/java/cn/hutool/Hutool.java | 38 + .../src/main/java/cn/hutool/package-info.java | 12 + hutool-aop/pom.xml | 38 + .../main/java/cn/hutool/aop/ProxyUtil.java | 73 + .../java/cn/hutool/aop/aspects/Aspect.java | 43 + .../cn/hutool/aop/aspects/SimpleAspect.java | 34 + .../aop/aspects/TimeIntervalAspect.java | 29 + .../cn/hutool/aop/aspects/package-info.java | 7 + .../aop/interceptor/CglibInterceptor.java | 60 + .../aop/interceptor/JdkInterceptor.java | 62 + .../hutool/aop/interceptor/package-info.java | 7 + .../main/java/cn/hutool/aop/package-info.java | 7 + .../hutool/aop/proxy/CglibProxyFactory.java | 25 + .../cn/hutool/aop/proxy/JdkProxyFactory.java | 21 + .../cn/hutool/aop/proxy/ProxyFactory.java | 64 + .../cn/hutool/aop/proxy/package-info.java | 7 + .../test/java/cn/hutool/aop/test/AopTest.java | 60 + hutool-bloomFilter/pom.xml | 24 + .../hutool/bloomfilter/BitMapBloomFilter.java | 79 + .../hutool/bloomfilter/BitSetBloomFilter.java | 145 + .../cn/hutool/bloomfilter/BloomFilter.java | 29 + .../hutool/bloomfilter/BloomFilterUtil.java | 32 + .../cn/hutool/bloomfilter/bitMap/BitMap.java | 34 + .../cn/hutool/bloomfilter/bitMap/IntMap.java | 56 + .../cn/hutool/bloomfilter/bitMap/LongMap.java | 56 + .../bloomfilter/bitMap/package-info.java | 7 + .../bloomfilter/filter/AbstractFilter.java | 83 + .../bloomfilter/filter/DefaultFilter.java | 25 + .../hutool/bloomfilter/filter/ELFFilter.java | 21 + .../hutool/bloomfilter/filter/FNVFilter.java | 21 + .../hutool/bloomfilter/filter/HfFilter.java | 31 + .../hutool/bloomfilter/filter/HfIpFilter.java | 24 + .../hutool/bloomfilter/filter/JSFilter.java | 30 + .../hutool/bloomfilter/filter/PJWFilter.java | 21 + .../hutool/bloomfilter/filter/RSFilter.java | 21 + .../hutool/bloomfilter/filter/SDBMFilter.java | 21 + .../bloomfilter/filter/TianlFilter.java | 22 + .../bloomfilter/filter/package-info.java | 7 + .../cn/hutool/bloomfilter/package-info.java | 7 + .../bloomfilter/BitMapBloomFilterTest.java | 55 + hutool-bom/pom.xml | 111 + hutool-cache/pom.xml | 24 + .../src/main/java/cn/hutool/cache/Cache.java | 149 + .../main/java/cn/hutool/cache/CacheUtil.java | 129 + .../cn/hutool/cache/GlobalPruneTimer.java | 83 + .../hutool/cache/file/AbstractFileCache.java | 134 + .../cn/hutool/cache/file/LFUFileCache.java | 64 + .../cn/hutool/cache/file/LRUFileCache.java | 64 + .../cn/hutool/cache/file/package-info.java | 7 + .../cn/hutool/cache/impl/AbstractCache.java | 359 ++ .../java/cn/hutool/cache/impl/CacheObj.java | 92 + .../hutool/cache/impl/CacheObjIterator.java | 74 + .../cache/impl/CacheValuesIterator.java | 49 + .../java/cn/hutool/cache/impl/FIFOCache.java | 78 + .../java/cn/hutool/cache/impl/LFUCache.java | 96 + .../java/cn/hutool/cache/impl/LRUCache.java | 71 + .../java/cn/hutool/cache/impl/NoCache.java | 102 + .../java/cn/hutool/cache/impl/TimedCache.java | 92 + .../java/cn/hutool/cache/impl/WeakCache.java | 24 + .../cn/hutool/cache/impl/package-info.java | 7 + .../java/cn/hutool/cache/package-info.java | 7 + .../cache/test/CacheConcurrentTest.java | 101 + .../java/cn/hutool/cache/test/CacheTest.java | 111 + .../cn/hutool/cache/test/FileCacheTest.java | 19 + hutool-captcha/pom.xml | 24 + .../cn/hutool/captcha/AbstractCaptcha.java | 226 + .../java/cn/hutool/captcha/CaptchaUtil.java | 86 + .../java/cn/hutool/captcha/CircleCaptcha.java | 101 + .../main/java/cn/hutool/captcha/ICaptcha.java | 40 + .../java/cn/hutool/captcha/LineCaptcha.java | 96 + .../java/cn/hutool/captcha/ShearCaptcha.java | 201 + .../captcha/generator/AbstractGenerator.java | 48 + .../captcha/generator/CodeGenerator.java | 28 + .../captcha/generator/MathGenerator.java | 96 + .../captcha/generator/RandomGenerator.java | 47 + .../captcha/generator/package-info.java | 7 + .../java/cn/hutool/captcha/package-info.java | 7 + .../java/cn/hutool/captcha/CaptchaTest.java | 95 + .../cn/hutool/captcha/CaptchaUtilTest.java | 15 + hutool-core/pom.xml | 19 + .../core/annotation/AnnotationUtil.java | 196 + .../CombinationAnnotationElement.java | 127 + .../hutool/core/annotation/package-info.java | 7 + .../java/cn/hutool/core/bean/BeanDesc.java | 458 ++ .../cn/hutool/core/bean/BeanDescCache.java | 33 + .../cn/hutool/core/bean/BeanException.java | 32 + .../cn/hutool/core/bean/BeanInfoCache.java | 39 + .../java/cn/hutool/core/bean/BeanPath.java | 295 ++ .../java/cn/hutool/core/bean/BeanUtil.java | 714 +++ .../java/cn/hutool/core/bean/DynaBean.java | 195 + .../hutool/core/bean/copier/BeanCopier.java | 300 ++ .../hutool/core/bean/copier/CopyOptions.java | 177 + .../core/bean/copier/ValueProvider.java | 36 + .../hutool/core/bean/copier/package-info.java | 7 + .../copier/provider/BeanValueProvider.java | 66 + .../copier/provider/MapValueProvider.java | 59 + .../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 | 976 ++++ .../cn/hutool/core/builder/EqualsBuilder.java | 878 ++++ .../hutool/core/builder/HashCodeBuilder.java | 958 ++++ .../java/cn/hutool/core/builder/IDKey.java | 63 + .../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 + .../cn/hutool/core/clone/package-info.java | 7 + .../main/java/cn/hutool/core/codec/BCD.java | 119 + .../java/cn/hutool/core/codec/Base32.java | 187 + .../java/cn/hutool/core/codec/Base62.java | 149 + .../cn/hutool/core/codec/Base62Codec.java | 180 + .../java/cn/hutool/core/codec/Base64.java | 358 ++ .../cn/hutool/core/codec/Base64Decoder.java | 172 + .../cn/hutool/core/codec/Base64Encoder.java | 194 + .../java/cn/hutool/core/codec/Caesar.java | 84 + .../main/java/cn/hutool/core/codec/Morse.java | 172 + .../main/java/cn/hutool/core/codec/Rot.java | 173 + .../cn/hutool/core/codec/package-info.java | 7 + .../cn/hutool/core/collection/ArrayIter.java | 115 + .../core/collection/BoundedPriorityQueue.java | 96 + .../cn/hutool/core/collection/CollUtil.java | 2514 ++++++++++ .../core/collection/CollectionUtil.java | 10 + .../core/collection/ConcurrentHashSet.java | 115 + .../cn/hutool/core/collection/CopiedIter.java | 70 + .../core/collection/EnumerationIter.java | 47 + .../cn/hutool/core/collection/IterUtil.java | 581 +++ .../core/collection/IteratorEnumeration.java | 37 + .../cn/hutool/core/collection/LineIter.java | 163 + .../hutool/core/collection/package-info.java | 7 + .../core/comparator/ComparableComparator.java | 54 + .../core/comparator/ComparatorChain.java | 280 ++ .../core/comparator/ComparatorException.java | 32 + .../hutool/core/comparator/CompareUtil.java | 80 + .../core/comparator/FieldComparator.java | 69 + .../core/comparator/IndexedComparator.java | 41 + .../core/comparator/PinyinComparator.java | 31 + .../core/comparator/PropertyComparator.java | 74 + .../core/comparator/ReverseComparator.java | 49 + .../core/comparator/VersionComparator.java | 102 + .../hutool/core/comparator/package-info.java | 7 + .../core/convert/AbstractConverter.java | 116 + .../cn/hutool/core/convert/BasicType.java | 59 + .../java/cn/hutool/core/convert/Convert.java | 1014 ++++ .../hutool/core/convert/ConvertException.java | 32 + .../cn/hutool/core/convert/Converter.java | 22 + .../core/convert/ConverterRegistry.java | 399 ++ .../core/convert/NumberChineseFormater.java | 172 + .../core/convert/NumberWordFormater.java | 143 + .../core/convert/impl/ArrayConverter.java | 152 + .../convert/impl/AtomicBooleanConverter.java | 29 + .../impl/AtomicReferenceConverter.java | 36 + .../core/convert/impl/BeanConverter.java | 83 + .../core/convert/impl/BooleanConverter.java | 23 + .../core/convert/impl/CalendarConverter.java | 57 + .../core/convert/impl/CastConverter.java | 28 + .../core/convert/impl/CharacterConverter.java | 33 + .../core/convert/impl/CharsetConverter.java | 21 + .../core/convert/impl/ClassConverter.java | 26 + .../convert/impl/CollectionConverter.java | 83 + .../core/convert/impl/CurrencyConverter.java | 21 + .../core/convert/impl/DateConverter.java | 101 + .../core/convert/impl/EnumConverter.java | 35 + .../convert/impl/GenericEnumConverter.java | 35 + .../core/convert/impl/Jdk8DateConverter.java | 152 + .../core/convert/impl/LocaleConverter.java | 41 + .../core/convert/impl/MapConverter.java | 97 + .../core/convert/impl/NumberConverter.java | 209 + .../core/convert/impl/PathConverter.java | 41 + .../core/convert/impl/PrimitiveConverter.java | 153 + .../core/convert/impl/ReferenceConverter.java | 56 + .../impl/StackTraceElementConverter.java | 34 + .../core/convert/impl/StringConverter.java | 18 + .../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/BetweenFormater.java | 174 + .../java/cn/hutool/core/date/DateBetween.java | 160 + .../cn/hutool/core/date/DateException.java | 32 + .../java/cn/hutool/core/date/DateField.java | 158 + .../cn/hutool/core/date/DateModifier.java | 146 + .../java/cn/hutool/core/date/DatePattern.java | 98 + .../java/cn/hutool/core/date/DateRange.java | 63 + .../java/cn/hutool/core/date/DateTime.java | 916 ++++ .../java/cn/hutool/core/date/DateUnit.java | 33 + .../java/cn/hutool/core/date/DateUtil.java | 1923 ++++++++ .../main/java/cn/hutool/core/date/Month.java | 118 + .../java/cn/hutool/core/date/Quarter.java | 61 + .../main/java/cn/hutool/core/date/Season.java | 64 + .../java/cn/hutool/core/date/SystemClock.java | 94 + .../cn/hutool/core/date/TimeInterval.java | 113 + .../main/java/cn/hutool/core/date/Week.java | 131 + .../main/java/cn/hutool/core/date/Zodiac.java | 103 + .../core/date/format/AbstractDateBasic.java | 64 + .../cn/hutool/core/date/format/DateBasic.java | 33 + .../hutool/core/date/format/DateParser.java | 68 + .../hutool/core/date/format/DatePrinter.java | 78 + .../core/date/format/FastDateFormat.java | 396 ++ .../core/date/format/FastDateParser.java | 834 ++++ .../core/date/format/FastDatePrinter.java | 1327 ++++++ .../hutool/core/date/format/FormatCache.java | 252 + .../hutool/core/date/format/package-info.java | 7 + .../cn/hutool/core/date/package-info.java | 7 + .../core/exceptions/DependencyException.java | 33 + .../hutool/core/exceptions/ExceptionUtil.java | 384 ++ .../core/exceptions/NotInitedException.java | 32 + .../core/exceptions/StatefulException.java | 57 + .../hutool/core/exceptions/UtilException.java | 31 + .../core/exceptions/ValidateException.java | 43 + .../hutool/core/exceptions/package-info.java | 7 + .../hutool/core/getter/ArrayTypeGetter.java | 102 + .../hutool/core/getter/BasicTypeGetter.java | 127 + .../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 | 136 + .../OptNullBasicTypeFromStringGetter.java | 84 + .../core/getter/OptNullBasicTypeGetter.java | 177 + .../cn/hutool/core/getter/package-info.java | 7 + .../java/cn/hutool/core/img/GraphicsUtil.java | 118 + .../src/main/java/cn/hutool/core/img/Img.java | 684 +++ .../main/java/cn/hutool/core/img/ImgUtil.java | 1844 ++++++++ .../java/cn/hutool/core/img/ScaleType.java | 43 + .../java/cn/hutool/core/img/package-info.java | 7 + .../cn/hutool/core/io/BOMInputStream.java | 117 + .../java/cn/hutool/core/io/BufferUtil.java | 251 + .../core/io/FastByteArrayOutputStream.java | 114 + .../cn/hutool/core/io/FastByteBuffer.java | 285 ++ .../java/cn/hutool/core/io/FileTypeUtil.java | 159 + .../main/java/cn/hutool/core/io/FileUtil.java | 3516 ++++++++++++++ .../cn/hutool/core/io/IORuntimeException.java | 47 + .../main/java/cn/hutool/core/io/IoUtil.java | 1139 +++++ .../java/cn/hutool/core/io/LineHandler.java | 14 + .../cn/hutool/core/io/NullOutputStream.java | 53 + .../cn/hutool/core/io/StreamProgress.java | 25 + .../cn/hutool/core/io/checksum/CRC16.java | 76 + .../java/cn/hutool/core/io/checksum/CRC8.java | 72 + .../hutool/core/io/checksum/package-info.java | 7 + .../cn/hutool/core/io/file/FileAppender.java | 87 + .../cn/hutool/core/io/file/FileCopier.java | 283 ++ .../java/cn/hutool/core/io/file/FileMode.java | 19 + .../cn/hutool/core/io/file/FileReader.java | 294 ++ .../cn/hutool/core/io/file/FileWrapper.java | 83 + .../cn/hutool/core/io/file/FileWriter.java | 391 ++ .../hutool/core/io/file/LineReadWatcher.java | 71 + .../cn/hutool/core/io/file/LineSeparator.java | 35 + .../java/cn/hutool/core/io/file/Tailer.java | 223 + .../cn/hutool/core/io/file/package-info.java | 7 + .../java/cn/hutool/core/io/package-info.java | 7 + .../core/io/resource/BytesResource.java | 83 + .../core/io/resource/ClassPathResource.java | 145 + .../hutool/core/io/resource/FileResource.java | 59 + .../core/io/resource/InputStreamResource.java | 93 + .../core/io/resource/MultiFileResource.java | 66 + .../core/io/resource/MultiResource.java | 127 + .../core/io/resource/NoResourceException.java | 49 + .../cn/hutool/core/io/resource/Resource.java | 73 + .../hutool/core/io/resource/ResourceUtil.java | 190 + .../core/io/resource/StringResource.java | 95 + .../hutool/core/io/resource/UrlResource.java | 130 + .../core/io/resource/WebAppResource.java | 25 + .../hutool/core/io/resource/package-info.java | 7 + .../hutool/core/io/watch/SimpleWatcher.java | 13 + .../hutool/core/io/watch/WatchException.java | 33 + .../cn/hutool/core/io/watch/WatchMonitor.java | 473 ++ .../cn/hutool/core/io/watch/WatchUtil.java | 380 ++ .../java/cn/hutool/core/io/watch/Watcher.java | 39 + .../cn/hutool/core/io/watch/package-info.java | 7 + .../core/io/watch/watchers/DelayWatcher.java | 108 + .../core/io/watch/watchers/IgnoreWatcher.java | 32 + .../core/io/watch/watchers/WatcherChain.java | 80 + .../core/io/watch/watchers/package-info.java | 7 + .../main/java/cn/hutool/core/lang/Assert.java | 655 +++ .../main/java/cn/hutool/core/lang/Chain.java | 17 + .../java/cn/hutool/core/lang/ClassScaner.java | 334 ++ .../cn/hutool/core/lang/ConsistentHash.java | 113 + .../java/cn/hutool/core/lang/Console.java | 182 + .../main/java/cn/hutool/core/lang/Dict.java | 465 ++ .../main/java/cn/hutool/core/lang/Editor.java | 23 + .../main/java/cn/hutool/core/lang/Filter.java | 15 + .../main/java/cn/hutool/core/lang/Holder.java | 44 + .../cn/hutool/core/lang/JarClassLoader.java | 166 + .../java/cn/hutool/core/lang/Matcher.java | 16 + .../java/cn/hutool/core/lang/MurmurHash.java | 351 ++ .../java/cn/hutool/core/lang/ObjectId.java | 185 + .../main/java/cn/hutool/core/lang/Pair.java | 53 + .../core/lang/ParameterizedTypeImpl.java | 103 + .../java/cn/hutool/core/lang/PatternPool.java | 170 + .../main/java/cn/hutool/core/lang/Range.java | 212 + .../java/cn/hutool/core/lang/Replacer.java | 21 + .../java/cn/hutool/core/lang/SimpleCache.java | 118 + .../java/cn/hutool/core/lang/Singleton.java | 112 + .../java/cn/hutool/core/lang/Snowflake.java | 174 + .../main/java/cn/hutool/core/lang/Tuple.java | 84 + .../cn/hutool/core/lang/TypeReference.java | 51 + .../main/java/cn/hutool/core/lang/UUID.java | 449 ++ .../java/cn/hutool/core/lang/Validator.java | 1069 +++++ .../cn/hutool/core/lang/WeightRandom.java | 236 + .../cn/hutool/core/lang/caller/Caller.java | 47 + .../hutool/core/lang/caller/CallerUtil.java | 81 + .../lang/caller/SecurityManagerCaller.java | 56 + .../core/lang/caller/StackTraceCaller.java | 70 + .../hutool/core/lang/caller/package-info.java | 7 + .../cn/hutool/core/lang/copier/Copier.java | 15 + .../core/lang/copier/SrcToDestCopier.java | 86 + .../hutool/core/lang/copier/package-info.java | 7 + .../java/cn/hutool/core/lang/func/Func.java | 25 + .../java/cn/hutool/core/lang/func/Func0.java | 22 + .../java/cn/hutool/core/lang/func/Func1.java | 25 + .../cn/hutool/core/lang/func/VoidFunc.java | 24 + .../cn/hutool/core/lang/func/VoidFunc0.java | 21 + .../cn/hutool/core/lang/func/VoidFunc1.java | 22 + .../hutool/core/lang/func/package-info.java | 10 + .../hutool/core/lang/loader/AtomicLoader.java | 51 + .../hutool/core/lang/loader/LazyLoader.java | 45 + .../cn/hutool/core/lang/loader/Loader.java | 20 + .../hutool/core/lang/loader/package-info.java | 7 + .../cn/hutool/core/lang/mutable/Mutable.java | 23 + .../hutool/core/lang/mutable/MutableBool.java | 103 + .../hutool/core/lang/mutable/MutableByte.java | 201 + .../core/lang/mutable/MutableDouble.java | 195 + .../core/lang/mutable/MutableFloat.java | 196 + .../hutool/core/lang/mutable/MutableInt.java | 196 + .../hutool/core/lang/mutable/MutableLong.java | 196 + .../hutool/core/lang/mutable/MutableObj.java | 71 + .../core/lang/mutable/MutableShort.java | 201 + .../core/lang/mutable/package-info.java | 7 + .../cn/hutool/core/lang/package-info.java | 7 + .../hutool/core/map/CamelCaseLinkedMap.java | 66 + .../java/cn/hutool/core/map/CamelCaseMap.java | 82 + .../core/map/CaseInsensitiveLinkedMap.java | 67 + .../hutool/core/map/CaseInsensitiveMap.java | 81 + .../java/cn/hutool/core/map/CustomKeyMap.java | 58 + .../hutool/core/map/FixedLinkedHashMap.java | 53 + .../java/cn/hutool/core/map/MapBuilder.java | 116 + .../java/cn/hutool/core/map/MapProxy.java | 178 + .../main/java/cn/hutool/core/map/MapUtil.java | 906 ++++ .../java/cn/hutool/core/map/MapWrapper.java | 111 + .../java/cn/hutool/core/map/TableMap.java | 154 + .../core/map/multi/CollectionValueMap.java | 109 + .../hutool/core/map/multi/ListValueMap.java | 78 + .../cn/hutool/core/map/multi/SetValueMap.java | 78 + .../hutool/core/map/multi/package-info.java | 7 + .../java/cn/hutool/core/map/package-info.java | 7 + .../java/cn/hutool/core/math/Arrangement.java | 133 + .../java/cn/hutool/core/math/Combination.java | 110 + .../java/cn/hutool/core/math/MathUtil.java | 79 + .../cn/hutool/core/math/package-info.java | 7 + .../hutool/core/net/LocalPortGenerater.java | 43 + .../main/java/cn/hutool/core/net/NetUtil.java | 660 +++ .../java/cn/hutool/core/net/URLEncoder.java | 235 + .../java/cn/hutool/core/net/package-info.java | 7 + .../java/cn/hutool/core/package-info.java | 7 + .../cn/hutool/core/swing/DesktopUtil.java | 97 + .../java/cn/hutool/core/swing/RobotUtil.java | 206 + .../java/cn/hutool/core/swing/ScreenUtil.java | 88 + .../swing/clipboard/ClipboardListener.java | 23 + .../swing/clipboard/ClipboardMonitor.java | 207 + .../core/swing/clipboard/ClipboardUtil.java | 177 + .../core/swing/clipboard/ImageSelection.java | 65 + .../swing/clipboard/StrClipboardListener.java | 34 + .../core/swing/clipboard/package-info.java | 7 + .../cn/hutool/core/swing/package-info.java | 7 + .../cn/hutool/core/text/ASCIIStrCache.java | 30 + .../java/cn/hutool/core/text/Simhash.java | 199 + .../java/cn/hutool/core/text/StrBuilder.java | 539 +++ .../cn/hutool/core/text/StrFormatter.java | 76 + .../java/cn/hutool/core/text/StrSpliter.java | 513 ++ .../cn/hutool/core/text/TextSimilarity.java | 132 + .../java/cn/hutool/core/text/UnicodeUtil.java | 93 + .../cn/hutool/core/text/csv/CsvConfig.java | 38 + .../java/cn/hutool/core/text/csv/CsvData.java | 72 + .../cn/hutool/core/text/csv/CsvParser.java | 266 ++ .../hutool/core/text/csv/CsvReadConfig.java | 56 + .../cn/hutool/core/text/csv/CsvReader.java | 172 + .../java/cn/hutool/core/text/csv/CsvRow.java | 253 + .../java/cn/hutool/core/text/csv/CsvUtil.java | 110 + .../hutool/core/text/csv/CsvWriteConfig.java | 47 + .../cn/hutool/core/text/csv/CsvWriter.java | 322 ++ .../cn/hutool/core/text/csv/package-info.java | 7 + .../hutool/core/text/escape/Html4Escape.java | 322 ++ .../core/text/escape/Html4Unescape.java | 25 + .../core/text/escape/InternalEscapeUtil.java | 24 + .../text/escape/NumericEntityUnescaper.java | 56 + .../hutool/core/text/escape/package-info.java | 7 + .../cn/hutool/core/text/package-info.java | 7 + .../core/text/replacer/LookupReplacer.java | 74 + .../core/text/replacer/ReplacerChain.java | 55 + .../core/text/replacer/StrReplacer.java | 44 + .../core/text/replacer/package-info.java | 7 + .../hutool/core/thread/ConcurrencyTester.java | 53 + .../hutool/core/thread/ExecutorBuilder.java | 211 + .../hutool/core/thread/GlobalThreadPool.java | 96 + .../core/thread/NamedThreadFactory.java | 98 + .../cn/hutool/core/thread/RejectPolicy.java | 40 + .../hutool/core/thread/SemaphoreRunnable.java | 47 + .../cn/hutool/core/thread/SyncFinisher.java | 194 + .../core/thread/ThreadFactoryBuilder.java | 148 + .../cn/hutool/core/thread/ThreadUtil.java | 457 ++ .../cn/hutool/core/thread/lock/NoLock.java | 42 + .../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 | 3830 +++++++++++++++ .../java/cn/hutool/core/util/BooleanUtil.java | 447 ++ .../java/cn/hutool/core/util/CharUtil.java | 322 ++ .../java/cn/hutool/core/util/CharsetUtil.java | 136 + .../cn/hutool/core/util/ClassLoaderUtil.java | 293 ++ .../java/cn/hutool/core/util/ClassUtil.java | 1021 ++++ .../java/cn/hutool/core/util/EnumUtil.java | 279 ++ .../java/cn/hutool/core/util/EscapeUtil.java | 129 + .../java/cn/hutool/core/util/HashUtil.java | 476 ++ .../java/cn/hutool/core/util/HexUtil.java | 385 ++ .../main/java/cn/hutool/core/util/IdUtil.java | 133 + .../java/cn/hutool/core/util/IdcardUtil.java | 592 +++ .../java/cn/hutool/core/util/ImageUtil.java | 17 + .../cn/hutool/core/util/ModifierUtil.java | 210 + .../java/cn/hutool/core/util/NumberUtil.java | 2335 +++++++++ .../java/cn/hutool/core/util/ObjectUtil.java | 536 +++ .../java/cn/hutool/core/util/PageUtil.java | 135 + .../java/cn/hutool/core/util/PinyinUtil.java | 204 + .../java/cn/hutool/core/util/RandomUtil.java | 524 +++ .../main/java/cn/hutool/core/util/ReUtil.java | 727 +++ .../cn/hutool/core/util/ReferenceUtil.java | 75 + .../java/cn/hutool/core/util/ReflectUtil.java | 813 ++++ .../java/cn/hutool/core/util/RuntimeUtil.java | 253 + .../java/cn/hutool/core/util/StrUtil.java | 4191 +++++++++++++++++ .../java/cn/hutool/core/util/TypeUtil.java | 359 ++ .../java/cn/hutool/core/util/URLUtil.java | 630 +++ .../java/cn/hutool/core/util/XmlUtil.java | 855 ++++ .../java/cn/hutool/core/util/ZipUtil.java | 1019 ++++ .../cn/hutool/core/util/package-info.java | 7 + .../core/annotation/AnnotationForTest.java | 27 + .../core/annotation/AnnotationUtilTest.java | 18 + .../cn/hutool/core/bean/BeanDescTest.java | 128 + .../cn/hutool/core/bean/BeanPathTest.java | 101 + .../cn/hutool/core/bean/BeanUtilTest.java | 303 ++ .../cn/hutool/core/bean/DynaBeanTest.java | 62 + .../java/cn/hutool/core/clone/CloneTest.java | 128 + .../java/cn/hutool/core/codec/BCDTest.java | 20 + .../java/cn/hutool/core/codec/Base32Test.java | 19 + .../java/cn/hutool/core/codec/Base62Test.java | 23 + .../java/cn/hutool/core/codec/Base64Test.java | 46 + .../java/cn/hutool/core/codec/CaesarTest.java | 18 + .../java/cn/hutool/core/codec/MorseTest.java | 33 + .../java/cn/hutool/core/codec/RotTest.java | 18 + .../hutool/core/collection/CollUtilTest.java | 574 +++ .../hutool/core/collection/IterUtilTest.java | 77 + .../hutool/core/collection/MapProxyTest.java | 31 + .../comparator/VersionComparatorTest.java | 49 + .../hutool/core/convert/ConvertOtherTest.java | 89 + .../cn/hutool/core/convert/ConvertTest.java | 205 + .../core/convert/ConvertToArrayTest.java | 135 + .../core/convert/ConvertToBeanTest.java | 50 + .../core/convert/ConvertToCollectionTest.java | 139 + .../core/convert/ConvertToSBCAndDBCTest.java | 30 + .../core/convert/ConverterRegistryTest.java | 43 + .../hutool/core/convert/MapConvertTest.java | 61 + .../convert/NumberChineseFormaterTest.java | 68 + .../core/convert/NumberWordFormatTest.java | 18 + .../hutool/core/date/BetweenFormaterTest.java | 29 + .../cn/hutool/core/date/DateBetweenTest.java | 50 + .../cn/hutool/core/date/DateFieldTest.java | 15 + .../cn/hutool/core/date/DateModifierTest.java | 109 + .../cn/hutool/core/date/DateTimeTest.java | 100 + .../cn/hutool/core/date/DateUtilTest.java | 563 +++ .../cn/hutool/core/date/TimeZoneTest.java | 23 + .../java/cn/hutool/core/date/ZodiacTest.java | 21 + .../core/exceptions/ExceptionUtilTest.java | 37 + .../test/java/cn/hutool/core/img/ImgTest.java | 27 + .../java/cn/hutool/core/img/ImgUtilTest.java | 105 + .../cn/hutool/core/io/BufferUtilTest.java | 66 + .../hutool/core/io/ClassPathResourceTest.java | 62 + .../cn/hutool/core/io/FileCopierTest.java | 45 + .../cn/hutool/core/io/FileReaderTest.java | 21 + .../cn/hutool/core/io/FileTypeUtilTest.java | 39 + .../java/cn/hutool/core/io/FileUtilTest.java | 323 ++ .../cn/hutool/core/io/WatchMonitorTest.java | 54 + .../cn/hutool/core/io/checksum/CrcTest.java | 35 + .../cn/hutool/core/io/file/TailerTest.java | 23 + .../java/cn/hutool/core/lang/AssertTest.java | 17 + .../java/cn/hutool/core/lang/CallerTest.java | 38 + .../cn/hutool/core/lang/ClassScanerTest.java | 19 + .../java/cn/hutool/core/lang/ConsoleTest.java | 60 + .../java/cn/hutool/core/lang/DictTest.java | 20 + .../cn/hutool/core/lang/ObjectIdTest.java | 33 + .../java/cn/hutool/core/lang/RangeTest.java | 57 + .../java/cn/hutool/core/lang/SimhashTest.java | 24 + .../cn/hutool/core/lang/SnowflakeTest.java | 46 + .../cn/hutool/core/lang/StrFormatterTest.java | 24 + .../cn/hutool/core/lang/StrSpliterTest.java | 48 + .../hutool/core/lang/TextSimilarityTest.java | 26 + .../cn/hutool/core/lang/ValidatorTest.java | 119 + .../cn/hutool/core/lang/WeightRandomTest.java | 20 + .../core/lang/test/bean/ExamInfoDict.java | 66 + .../core/lang/test/bean/UserInfoDict.java | 79 + .../lang/test/bean/UserInfoRedundCount.java | 38 + .../cn/hutool/core/map/CamelCaseMapTest.java | 23 + .../core/map/CaseInsensitiveMapTest.java | 23 + .../java/cn/hutool/core/map/MapUtilTest.java | 100 + .../cn/hutool/core/math/ArrangementTest.java | 54 + .../cn/hutool/core/math/CombinationTest.java | 54 + .../java/cn/hutool/core/net/NetUtilTest.java | 57 + .../core/swing/ClipboardMonitorTest.java | 43 + .../hutool/core/swing/ClipboardUtilTest.java | 28 + .../cn/hutool/core/swing/DesktopUtilTest.java | 13 + .../cn/hutool/core/swing/RobotUtilTest.java | 15 + .../cn/hutool/core/text/StrBuilderTest.java | 89 + .../cn/hutool/core/text/UnicodeUtilTest.java | 49 + .../hutool/core/text/csv/CsvParserTest.java | 48 + .../hutool/core/text/csv/CsvReaderTest.java | 17 + .../cn/hutool/core/text/csv/CsvUtilTest.java | 36 + .../core/thread/ConcurrencyTesterTest.java | 25 + .../cn/hutool/core/thread/ThreadUtilTest.java | 21 + .../cn/hutool/core/util/ArrayUtilTest.java | 228 + .../cn/hutool/core/util/BooleanUtilTest.java | 24 + .../cn/hutool/core/util/CharUtilTest.java | 23 + .../hutool/core/util/ClassLoaderUtilTest.java | 16 + .../cn/hutool/core/util/ClassUtilTest.java | 105 + .../cn/hutool/core/util/EnumUtilTest.java | 73 + .../cn/hutool/core/util/EscapeUtilTest.java | 17 + .../java/cn/hutool/core/util/HexUtilTest.java | 41 + .../java/cn/hutool/core/util/IdUtilTest.java | 143 + .../cn/hutool/core/util/IdcardUtilTest.java | 69 + .../cn/hutool/core/util/NumberUtilTest.java | 232 + .../cn/hutool/core/util/ObjectUtilTest.java | 32 + .../cn/hutool/core/util/PageUtilTest.java | 35 + .../cn/hutool/core/util/PinyinUtilTest.java | 52 + .../cn/hutool/core/util/RandomUtilTest.java | 39 + .../java/cn/hutool/core/util/ReUtilTest.java | 139 + .../cn/hutool/core/util/ReflectUtilTest.java | 105 + .../cn/hutool/core/util/RuntimeUtilTest.java | 29 + .../java/cn/hutool/core/util/StrUtilTest.java | 409 ++ .../cn/hutool/core/util/TypeUtilTest.java | 42 + .../java/cn/hutool/core/util/URLUtilTest.java | 64 + .../java/cn/hutool/core/util/XmlUtilTest.java | 119 + .../java/cn/hutool/core/util/ZipUtilTest.java | 95 + hutool-core/src/test/resources/hutool.jpg | Bin 0 -> 22807 bytes hutool-core/src/test/resources/test.csv | 1 + .../src/test/resources/test.properties | 6 + hutool-core/src/test/resources/test.xml | 8 + hutool-cron/pom.xml | 29 + .../java/cn/hutool/cron/CronException.java | 27 + .../main/java/cn/hutool/cron/CronTimer.java | 72 + .../main/java/cn/hutool/cron/CronUtil.java | 205 + .../main/java/cn/hutool/cron/Scheduler.java | 436 ++ .../java/cn/hutool/cron/TaskExecutor.java | 41 + .../cn/hutool/cron/TaskExecutorManager.java | 72 + .../java/cn/hutool/cron/TaskLauncher.java | 29 + .../cn/hutool/cron/TaskLauncherManager.java | 64 + .../main/java/cn/hutool/cron/TaskTable.java | 223 + .../cron/listener/SimpleTaskListener.java | 27 + .../cn/hutool/cron/listener/TaskListener.java | 32 + .../cron/listener/TaskListenerManager.java | 88 + .../cn/hutool/cron/listener/package-info.java | 7 + .../java/cn/hutool/cron/package-info.java | 7 + .../cn/hutool/cron/pattern/CronPattern.java | 297 ++ .../hutool/cron/pattern/CronPatternUtil.java | 103 + .../matcher/AlwaysTrueValueMatcher.java | 21 + .../matcher/BoolArrayValueMatcher.java | 36 + .../matcher/DayOfMonthValueMatcher.java | 58 + .../cron/pattern/matcher/ValueMatcher.java | 13 + .../pattern/matcher/ValueMatcherBuilder.java | 196 + .../pattern/matcher/YearValueMatcher.java | 23 + .../cron/pattern/matcher/package-info.java | 7 + .../cn/hutool/cron/pattern/package-info.java | 7 + .../pattern/parser/DayOfMonthValueParser.java | 26 + .../pattern/parser/DayOfWeekValueParser.java | 53 + .../cron/pattern/parser/HourValueParser.java | 14 + .../pattern/parser/MinuteValueParser.java | 14 + .../cron/pattern/parser/MonthValueParser.java | 43 + .../pattern/parser/SecondValueParser.java | 9 + .../pattern/parser/SimpleValueParser.java | 50 + .../cron/pattern/parser/ValueParser.java | 37 + .../cron/pattern/parser/YearValueParser.java | 14 + .../cron/pattern/parser/package-info.java | 7 + .../java/cn/hutool/cron/task/InvokeTask.java | 69 + .../cn/hutool/cron/task/RunnableTask.java | 19 + .../main/java/cn/hutool/cron/task/Task.java | 14 + .../cn/hutool/cron/task/package-info.java | 7 + .../cron/demo/AddAndRemoveMainTest.java | 32 + .../java/cn/hutool/cron/demo/CronTest.java | 72 + .../cn/hutool/cron/demo/DeamonMainTest.java | 19 + .../java/cn/hutool/cron/demo/JobMainTest.java | 14 + .../java/cn/hutool/cron/demo/TestJob.java | 36 + .../java/cn/hutool/cron/demo/TestJob2.java | 24 + .../hutool/cron/pattern/CronPatternTest.java | 138 + .../cron/pattern/CronPatternUtilTest.java | 48 + .../src/test/resources/config/cron.setting | 17 + hutool-crypto/pom.xml | 38 + .../main/java/cn/hutool/crypto/BCUtil.java | 168 + .../cn/hutool/crypto/CryptoException.java | 33 + .../crypto/GlobalBouncyCastleProvider.java | 42 + .../main/java/cn/hutool/crypto/KeyUtil.java | 770 +++ .../src/main/java/cn/hutool/crypto/Mode.java | 26 + .../main/java/cn/hutool/crypto/Padding.java | 18 + .../cn/hutool/crypto/ProviderFactory.java | 26 + .../java/cn/hutool/crypto/SecureUtil.java | 1046 ++++ .../main/java/cn/hutool/crypto/SmUtil.java | 275 ++ .../asymmetric/AbstractAsymmetricCrypto.java | 325 ++ .../asymmetric/AsymmetricAlgorithm.java | 37 + .../crypto/asymmetric/AsymmetricCrypto.java | 307 ++ .../crypto/asymmetric/BaseAsymmetric.java | 171 + .../cn/hutool/crypto/asymmetric/KeyType.java | 11 + .../java/cn/hutool/crypto/asymmetric/RSA.java | 219 + .../java/cn/hutool/crypto/asymmetric/SM2.java | 334 ++ .../hutool/crypto/asymmetric/SM2Engine.java | 359 ++ .../cn/hutool/crypto/asymmetric/Sign.java | 257 + .../crypto/asymmetric/SignAlgorithm.java | 55 + .../crypto/asymmetric/package-info.java | 7 + .../java/cn/hutool/crypto/digest/BCrypt.java | 521 ++ .../hutool/crypto/digest/DigestAlgorithm.java | 35 + .../cn/hutool/crypto/digest/DigestUtil.java | 486 ++ .../cn/hutool/crypto/digest/Digester.java | 462 ++ .../java/cn/hutool/crypto/digest/HMac.java | 226 + .../hutool/crypto/digest/HmacAlgorithm.java | 27 + .../java/cn/hutool/crypto/digest/MD5.java | 119 + .../crypto/digest/mac/BCHMacEngine.java | 97 + .../crypto/digest/mac/DefaultHMacEngine.java | 109 + .../hutool/crypto/digest/mac/MacEngine.java | 23 + .../crypto/digest/mac/MacEngineFactory.java | 29 + .../crypto/digest/mac/package-info.java | 10 + .../cn/hutool/crypto/digest/package-info.java | 7 + .../java/cn/hutool/crypto/package-info.java | 13 + .../java/cn/hutool/crypto/symmetric/AES.java | 177 + .../java/cn/hutool/crypto/symmetric/DES.java | 177 + .../cn/hutool/crypto/symmetric/DESede.java | 178 + .../java/cn/hutool/crypto/symmetric/RC4.java | 220 + .../crypto/symmetric/SymmetricAlgorithm.java | 42 + .../crypto/symmetric/SymmetricCrypto.java | 447 ++ .../cn/hutool/crypto/symmetric/Vigenere.java | 65 + .../hutool/crypto/symmetric/package-info.java | 7 + .../cn/hutool/crypto/test/BCUtilTest.java | 40 + .../java/cn/hutool/crypto/test/HmacTest.java | 58 + .../cn/hutool/crypto/test/KeyUtilTest.java | 26 + .../java/cn/hutool/crypto/test/RC4Test.java | 39 + .../java/cn/hutool/crypto/test/RSATest.java | 155 + .../java/cn/hutool/crypto/test/SM2Test.java | 142 + .../java/cn/hutool/crypto/test/SignTest.java | 91 + .../java/cn/hutool/crypto/test/SmTest.java | 51 + .../cn/hutool/crypto/test/SymmetricTest.java | 210 + .../hutool/crypto/test/digest/DigestTest.java | 76 + .../cn/hutool/crypto/test/digest/Md5Test.java | 22 + .../src/test/resources/test_private_key.pem | 15 + .../src/test/resources/test_public_key.csr | 15 + hutool-db/pom.xml | 139 + .../main/java/cn/hutool/db/AbstractDb.java | 879 ++++ .../main/java/cn/hutool/db/ActiveEntity.java | 232 + .../main/java/cn/hutool/db/DaoTemplate.java | 361 ++ hutool-db/src/main/java/cn/hutool/db/Db.java | 239 + .../java/cn/hutool/db/DbRuntimeException.java | 32 + .../src/main/java/cn/hutool/db/DbUtil.java | 262 ++ .../src/main/java/cn/hutool/db/Entity.java | 397 ++ .../src/main/java/cn/hutool/db/Page.java | 175 + .../main/java/cn/hutool/db/PageResult.java | 143 + .../src/main/java/cn/hutool/db/Session.java | 333 ++ .../main/java/cn/hutool/db/SqlConnRunner.java | 604 +++ .../src/main/java/cn/hutool/db/SqlRunner.java | 130 + .../main/java/cn/hutool/db/StatementUtil.java | 261 + .../cn/hutool/db/ThreadLocalConnection.java | 101 + .../java/cn/hutool/db/dialect/Dialect.java | 113 + .../cn/hutool/db/dialect/DialectFactory.java | 187 + .../cn/hutool/db/dialect/DialectName.java | 10 + .../java/cn/hutool/db/dialect/DriverUtil.java | 86 + .../db/dialect/impl/AnsiSqlDialect.java | 153 + .../cn/hutool/db/dialect/impl/H2Dialect.java | 31 + .../hutool/db/dialect/impl/MysqlDialect.java | 29 + .../hutool/db/dialect/impl/OracleDialect.java | 33 + .../db/dialect/impl/PostgresqlDialect.java | 23 + .../db/dialect/impl/SqlServer2012Dialect.java | 40 + .../db/dialect/impl/Sqlite3Dialect.java | 22 + .../hutool/db/dialect/impl/package-info.java | 7 + .../cn/hutool/db/dialect/package-info.java | 7 + .../cn/hutool/db/ds/AbstractDSFactory.java | 204 + .../main/java/cn/hutool/db/ds/DSFactory.java | 192 + .../cn/hutool/db/ds/DataSourceWrapper.java | 120 + .../java/cn/hutool/db/ds/GlobalDSFactory.java | 79 + .../cn/hutool/db/ds/c3p0/C3p0DSFactory.java | 55 + .../cn/hutool/db/ds/c3p0/package-info.java | 7 + .../cn/hutool/db/ds/dbcp/DbcpDSFactory.java | 41 + .../cn/hutool/db/ds/dbcp/package-info.java | 7 + .../cn/hutool/db/ds/druid/DruidDSFactory.java | 70 + .../cn/hutool/db/ds/druid/package-info.java | 7 + .../hutool/db/ds/hikari/HikariDSFactory.java | 50 + .../cn/hutool/db/ds/hikari/package-info.java | 7 + .../cn/hutool/db/ds/jndi/JndiDSFactory.java | 43 + .../cn/hutool/db/ds/jndi/package-info.java | 7 + .../java/cn/hutool/db/ds/package-info.java | 7 + .../hutool/db/ds/pooled/ConnectionWraper.java | 297 ++ .../java/cn/hutool/db/ds/pooled/DbConfig.java | 109 + .../cn/hutool/db/ds/pooled/DbSetting.java | 76 + .../hutool/db/ds/pooled/PooledConnection.java | 66 + .../hutool/db/ds/pooled/PooledDSFactory.java | 43 + .../hutool/db/ds/pooled/PooledDataSource.java | 189 + .../cn/hutool/db/ds/pooled/package-info.java | 7 + .../db/ds/simple/AbstractDataSource.java | 56 + .../hutool/db/ds/simple/SimpleDSFactory.java | 37 + .../hutool/db/ds/simple/SimpleDataSource.java | 197 + .../cn/hutool/db/ds/simple/package-info.java | 7 + .../hutool/db/ds/tomcat/TomcatDSFactory.java | 46 + .../cn/hutool/db/ds/tomcat/package-info.java | 7 + .../cn/hutool/db/handler/BeanHandler.java | 40 + .../cn/hutool/db/handler/BeanListHandler.java | 43 + .../cn/hutool/db/handler/EntityHandler.java | 33 + .../hutool/db/handler/EntityListHandler.java | 49 + .../hutool/db/handler/EntitySetHandler.java | 29 + .../cn/hutool/db/handler/HandleHelper.java | 299 ++ .../cn/hutool/db/handler/NumberHandler.java | 26 + .../hutool/db/handler/PageResultHandler.java | 42 + .../java/cn/hutool/db/handler/RsHandler.java | 32 + .../cn/hutool/db/handler/StringHandler.java | 26 + .../cn/hutool/db/handler/package-info.java | 7 + .../main/java/cn/hutool/db/meta/Column.java | 244 + .../main/java/cn/hutool/db/meta/JdbcType.java | 80 + .../main/java/cn/hutool/db/meta/MetaUtil.java | 255 + .../main/java/cn/hutool/db/meta/Table.java | 142 + .../java/cn/hutool/db/meta/TableType.java | 38 + .../java/cn/hutool/db/meta/package-info.java | 7 + .../cn/hutool/db/nosql/mongo/MongoDS.java | 405 ++ .../hutool/db/nosql/mongo/MongoFactory.java | 125 + .../hutool/db/nosql/mongo/package-info.java | 7 + .../java/cn/hutool/db/nosql/package-info.java | 7 + .../cn/hutool/db/nosql/redis/RedisDS.java | 180 + .../hutool/db/nosql/redis/package-info.java | 7 + .../main/java/cn/hutool/db/package-info.java | 8 + .../main/java/cn/hutool/db/sql/Condition.java | 493 ++ .../main/java/cn/hutool/db/sql/Direction.java | 32 + .../cn/hutool/db/sql/LogicalOperator.java | 29 + .../main/java/cn/hutool/db/sql/NamedSql.java | 135 + .../src/main/java/cn/hutool/db/sql/Order.java | 78 + .../src/main/java/cn/hutool/db/sql/Query.java | 182 + .../java/cn/hutool/db/sql/SqlBuilder.java | 632 +++ .../java/cn/hutool/db/sql/SqlExecutor.java | 355 ++ .../java/cn/hutool/db/sql/SqlFormatter.java | 327 ++ .../main/java/cn/hutool/db/sql/SqlLog.java | 56 + .../main/java/cn/hutool/db/sql/SqlUtil.java | 254 + .../cn/hutool/db/sql/StatementWrapper.java | 539 +++ .../main/java/cn/hutool/db/sql/Wrapper.java | 189 + .../java/cn/hutool/db/sql/package-info.java | 7 + .../db/transaction/TransactionLevel.java | 75 + .../hutool/db/transaction/package-info.java | 7 + .../src/test/java/cn/hutool/db/CRUDTest.java | 187 + .../test/java/cn/hutool/db/ConcurentTest.java | 54 + .../src/test/java/cn/hutool/db/DbTest.java | 56 + .../src/test/java/cn/hutool/db/DsTest.java | 92 + .../test/java/cn/hutool/db/EntityTest.java | 48 + .../test/java/cn/hutool/db/FindBeanTest.java | 71 + .../test/java/cn/hutool/db/HsqldbTest.java | 38 + .../src/test/java/cn/hutool/db/MySQLTest.java | 65 + .../test/java/cn/hutool/db/NamedSqlTest.java | 39 + .../test/java/cn/hutool/db/OracleTest.java | 69 + .../test/java/cn/hutool/db/PostgreTest.java | 41 + .../test/java/cn/hutool/db/SessionTest.java | 40 + .../test/java/cn/hutool/db/SqlServerTest.java | 48 + .../test/java/cn/hutool/db/UpdateTest.java | 37 + .../java/cn/hutool/db/meta/MetaUtilTest.java | 40 + .../src/test/java/cn/hutool/db/pojo/User.java | 60 + .../java/cn/hutool/db/sql/ConditionTest.java | 55 + .../java/cn/hutool/db/sql/SqlBuilderTest.java | 22 + .../src/test/resources/config/db.setting | 51 + .../config/example/db-example-c3p0.setting | 54 + .../config/example/db-example-dbcp.setting | 51 + .../config/example/db-example-druid.setting | 55 + .../config/example/db-example-hikari.setting | 43 + .../config/example/db-example-tomcat.setting | 51 + .../config/example/mongo-example.setting | 29 + .../src/test/resources/config/redis.setting | 59 + hutool-db/src/test/resources/logback.xml | 17 + .../test/resources/simplelogger.properties | 2 + hutool-db/test.db | Bin 0 -> 24576 bytes hutool-dfa/pom.xml | 29 + .../java/cn/hutool/dfa/SensitiveUtil.java | 161 + .../src/main/java/cn/hutool/dfa/StopChar.java | 49 + .../src/main/java/cn/hutool/dfa/WordTree.java | 233 + .../main/java/cn/hutool/dfa/package-info.java | 9 + .../test/java/cn/hutool/dfa/test/DfaTest.java | 113 + hutool-extra/pom.xml | 204 + .../java/cn/hutool/extra/emoji/EmojiUtil.java | 179 + .../cn/hutool/extra/emoji/package-info.java | 7 + .../java/cn/hutool/extra/ftp/AbstractFtp.java | 175 + .../main/java/cn/hutool/extra/ftp/Ftp.java | 531 +++ .../cn/hutool/extra/ftp/FtpException.java | 33 + .../java/cn/hutool/extra/ftp/FtpMode.java | 17 + .../cn/hutool/extra/ftp/package-info.java | 7 + .../hutool/extra/mail/GlobalMailAccount.java | 65 + .../hutool/extra/mail/InternalMailUtil.java | 108 + .../main/java/cn/hutool/extra/mail/Mail.java | 354 ++ .../cn/hutool/extra/mail/MailAccount.java | 489 ++ .../cn/hutool/extra/mail/MailException.java | 32 + .../java/cn/hutool/extra/mail/MailUtil.java | 236 + .../extra/mail/UserPassAuthenticator.java | 34 + .../cn/hutool/extra/mail/package-info.java | 7 + .../java/cn/hutool/extra/package-info.java | 7 + .../qrcode/BufferedImageLuminanceSource.java | 120 + .../hutool/extra/qrcode/QrCodeException.java | 33 + .../cn/hutool/extra/qrcode/QrCodeUtil.java | 352 ++ .../java/cn/hutool/extra/qrcode/QrConfig.java | 291 ++ .../cn/hutool/extra/qrcode/package-info.java | 7 + .../cn/hutool/extra/servlet/ServletUtil.java | 623 +++ .../servlet/multipart/MultipartFormData.java | 257 + .../MultipartRequestInputStream.java | 214 + .../extra/servlet/multipart/UploadFile.java | 271 ++ .../servlet/multipart/UploadFileHeader.java | 187 + .../servlet/multipart/UploadSetting.java | 149 + .../extra/servlet/multipart/package-info.java | 7 + .../cn/hutool/extra/servlet/package-info.java | 7 + .../java/cn/hutool/extra/ssh/ChannelType.java | 49 + .../java/cn/hutool/extra/ssh/Connector.java | 147 + .../extra/ssh/JschRuntimeException.java | 32 + .../cn/hutool/extra/ssh/JschSessionPool.java | 116 + .../java/cn/hutool/extra/ssh/JschUtil.java | 335 ++ .../main/java/cn/hutool/extra/ssh/Sftp.java | 425 ++ .../cn/hutool/extra/ssh/package-info.java | 7 + .../extra/template/AbstractTemplate.java | 36 + .../cn/hutool/extra/template/Template.java | 46 + .../hutool/extra/template/TemplateConfig.java | 189 + .../hutool/extra/template/TemplateEngine.java | 18 + .../extra/template/TemplateException.java | 33 + .../hutool/extra/template/TemplateUtil.java | 34 + .../template/engine/TemplateFactory.java | 77 + .../template/engine/beetl/BeetlEngine.java | 114 + .../template/engine/beetl/BeetlTemplate.java | 51 + .../template/engine/beetl/BeetlUtil.java | 289 ++ .../template/engine/beetl/package-info.java | 7 + .../template/engine/enjoy/EnjoyEngine.java | 96 + .../template/engine/enjoy/EnjoyTemplate.java | 50 + .../template/engine/enjoy/package-info.java | 7 + .../engine/freemarker/FreemarkerEngine.java | 105 + .../engine/freemarker/FreemarkerTemplate.java | 59 + .../SimpleStringTemplateLoader.java | 38 + .../engine/freemarker/package-info.java | 7 + .../extra/template/engine/package-info.java | 7 + .../template/engine/rythm/RythmEngine.java | 72 + .../template/engine/rythm/RythmTemplate.java | 55 + .../template/engine/rythm/package-info.java | 7 + .../engine/thymeleaf/ThymeleafEngine.java | 109 + .../engine/thymeleaf/ThymeleafTemplate.java | 70 + .../engine/thymeleaf/package-info.java | 7 + .../velocity/SimpleStringResourceLoader.java | 54 + .../engine/velocity/VelocityEngine.java | 108 + .../engine/velocity/VelocityTemplate.java | 81 + .../engine/velocity/VelocityUtil.java | 338 ++ .../engine/velocity/package-info.java | 7 + .../hutool/extra/template/package-info.java | 7 + .../extra/tokenizer/AbstractResult.java | 56 + .../cn/hutool/extra/tokenizer/Result.java | 14 + .../extra/tokenizer/TokenizerEngine.java | 18 + .../extra/tokenizer/TokenizerException.java | 33 + .../hutool/extra/tokenizer/TokenizerUtil.java | 21 + .../java/cn/hutool/extra/tokenizer/Word.java | 31 + .../tokenizer/engine/TokenizerFactory.java | 82 + .../engine/analysis/AnalysisEngine.java | 45 + .../engine/analysis/AnalysisResult.java | 43 + .../engine/analysis/AnalysisWord.java | 53 + .../engine/analysis/SmartcnEngine.java | 21 + .../engine/analysis/package-info.java | 8 + .../tokenizer/engine/ansj/AnsjEngine.java | 42 + .../tokenizer/engine/ansj/AnsjResult.java | 49 + .../extra/tokenizer/engine/ansj/AnsjWord.java | 44 + .../tokenizer/engine/ansj/package-info.java | 8 + .../tokenizer/engine/hanlp/HanLPEngine.java | 43 + .../tokenizer/engine/hanlp/HanLPResult.java | 47 + .../tokenizer/engine/hanlp/HanLPWord.java | 45 + .../tokenizer/engine/hanlp/package-info.java | 8 + .../engine/ikanalyzer/IKAnalyzerEngine.java | 43 + .../engine/ikanalyzer/IKAnalyzerResult.java | 45 + .../engine/ikanalyzer/IKAnalyzerWord.java | 45 + .../engine/ikanalyzer/package-info.java | 8 + .../tokenizer/engine/jcseg/JcsegEngine.java | 67 + .../tokenizer/engine/jcseg/JcsegResult.java | 72 + .../tokenizer/engine/jcseg/JcsegWord.java | 44 + .../tokenizer/engine/jcseg/package-info.java | 8 + .../tokenizer/engine/jieba/JiebaEngine.java | 44 + .../tokenizer/engine/jieba/JiebaResult.java | 50 + .../tokenizer/engine/jieba/JiebaWord.java | 44 + .../tokenizer/engine/jieba/package-info.java | 8 + .../tokenizer/engine/mmseg/MmsegEngine.java | 48 + .../tokenizer/engine/mmseg/MmsegResult.java | 44 + .../tokenizer/engine/mmseg/MmsegWord.java | 43 + .../tokenizer/engine/mmseg/package-info.java | 8 + .../extra/tokenizer/engine/package-info.java | 7 + .../tokenizer/engine/word/WordEngine.java | 52 + .../tokenizer/engine/word/WordResult.java | 49 + .../extra/tokenizer/engine/word/WordWord.java | 43 + .../tokenizer/engine/word/package-info.java | 8 + .../hutool/extra/tokenizer/package-info.java | 8 + .../cn/hutool/extra/emoji/EmojiUtilTest.java | 19 + .../java/cn/hutool/extra/ftp/FtpTest.java | 62 + .../cn/hutool/extra/mail/MailAccountTest.java | 21 + .../java/cn/hutool/extra/mail/MailTest.java | 59 + .../hutool/extra/qrcode/QrCodeUtilTest.java | 53 + .../cn/hutool/extra/ssh/JschUtilTest.java | 64 + .../hutool/extra/template/BeetlUtilTest.java | 35 + .../extra/template/TemplateUtilTest.java | 138 + .../hutool/extra/template/ThymeleafTest.java | 99 + .../extra/tokenizer/TokenizerUtilTest.java | 93 + .../src/test/resources/beetl.properties | 62 + .../src/test/resources/config/mail.setting | 18 + .../example/beetl-example.properties | 68 + .../resources/example/mail-example.setting | 40 + .../resources/example/velocity-example.vm | 52 + .../test/resources/templates/beetl_test.btl | 1 + .../test/resources/templates/enjoy_test.etl | 1 + .../resources/templates/freemarker_test.ftl | 1 + .../test/resources/templates/rythm_test.tmpl | 2 + .../resources/templates/thymeleaf_test.ttl | 1 + .../resources/templates/velocity_test.vtl | 1 + hutool-http/pom.xml | 42 + .../main/java/cn/hutool/http/ContentType.java | 110 + .../java/cn/hutool/http/GlobalHeaders.java | 211 + .../main/java/cn/hutool/http/HTMLFilter.java | 527 +++ .../src/main/java/cn/hutool/http/Header.java | 75 + .../main/java/cn/hutool/http/HtmlUtil.java | 203 + .../main/java/cn/hutool/http/HttpBase.java | 286 ++ .../java/cn/hutool/http/HttpConnection.java | 543 +++ .../java/cn/hutool/http/HttpException.java | 31 + .../java/cn/hutool/http/HttpGlobalConfig.java | 66 + .../main/java/cn/hutool/http/HttpRequest.java | 1159 +++++ .../java/cn/hutool/http/HttpResponse.java | 502 ++ .../main/java/cn/hutool/http/HttpStatus.java | 194 + .../main/java/cn/hutool/http/HttpUtil.java | 780 +++ .../src/main/java/cn/hutool/http/Method.java | 10 + .../src/main/java/cn/hutool/http/Status.java | 189 + .../http/cookie/GlobalCookieManager.java | 102 + .../http/cookie/ThreadLocalCookieStore.java | 75 + .../cn/hutool/http/cookie/package-info.java | 7 + .../java/cn/hutool/http/package-info.java | 7 + .../http/ssl/AndroidSupportSSLFactory.java | 28 + .../http/ssl/CustomProtocolsSSLFactory.java | 100 + .../hutool/http/ssl/DefaultTrustManager.java | 27 + .../http/ssl/SSLSocketFactoryBuilder.java | 113 + .../http/ssl/TrustAnyHostnameVerifier.java | 17 + .../java/cn/hutool/http/ssl/package-info.java | 7 + .../cn/hutool/http/useragent/Browser.java | 87 + .../java/cn/hutool/http/useragent/Engine.java | 43 + .../java/cn/hutool/http/useragent/OS.java | 59 + .../cn/hutool/http/useragent/Platform.java | 71 + .../cn/hutool/http/useragent/UserAgent.java | 152 + .../hutool/http/useragent/UserAgentInfo.java | 114 + .../http/useragent/UserAgentParser.java | 114 + .../hutool/http/useragent/UserAgentUtil.java | 20 + .../hutool/http/useragent/package-info.java | 7 + .../cn/hutool/http/webservice/SoapClient.java | 555 +++ .../hutool/http/webservice/SoapProtocol.java | 36 + .../http/webservice/SoapRuntimeException.java | 32 + .../cn/hutool/http/webservice/SoapUtil.java | 92 + .../hutool/http/webservice/package-info.java | 7 + .../cn/hutool/http/test/DownloadTest.java | 66 + .../cn/hutool/http/test/HtmlUtilTest.java | 96 + .../cn/hutool/http/test/HttpRequestTest.java | 92 + .../cn/hutool/http/test/HttpUtilTest.java | 265 ++ .../java/cn/hutool/http/test/RestTest.java | 50 + .../java/cn/hutool/http/test/UploadTest.java | 51 + .../http/useragent/UserAgentUtilTest.java | 151 + .../http/webservice/SoapClientTest.java | 44 + hutool-json/pom.xml | 32 + .../java/cn/hutool/json/InternalJSONUtil.java | 241 + .../src/main/java/cn/hutool/json/JSON.java | 128 + .../main/java/cn/hutool/json/JSONArray.java | 628 +++ .../main/java/cn/hutool/json/JSONConfig.java | 132 + .../java/cn/hutool/json/JSONConverter.java | 99 + .../java/cn/hutool/json/JSONException.java | 34 + .../main/java/cn/hutool/json/JSONGetter.java | 133 + .../main/java/cn/hutool/json/JSONNull.java | 59 + .../main/java/cn/hutool/json/JSONObject.java | 803 ++++ .../java/cn/hutool/json/JSONObjectIter.java | 40 + .../java/cn/hutool/json/JSONStrFormater.java | 123 + .../main/java/cn/hutool/json/JSONString.java | 18 + .../main/java/cn/hutool/json/JSONSupport.java | 45 + .../main/java/cn/hutool/json/JSONTokener.java | 422 ++ .../main/java/cn/hutool/json/JSONUtil.java | 737 +++ .../src/main/java/cn/hutool/json/XML.java | 369 ++ .../main/java/cn/hutool/json/XMLTokener.java | 328 ++ .../java/cn/hutool/json/package-info.java | 7 + .../java/cn/hutool/json/JSONArrayTest.java | 199 + .../java/cn/hutool/json/JSONConvertTest.java | 106 + .../java/cn/hutool/json/JSONObjectTest.java | 460 ++ .../java/cn/hutool/json/JSONPathTest.java | 22 + .../cn/hutool/json/JSONStrFormaterTest.java | 33 + .../java/cn/hutool/json/JSONUtilTest.java | 130 + .../java/cn/hutool/json/ParseBeanTest.java | 78 + .../cn/hutool/json/issueIVMD5/BaseResult.java | 20 + .../json/issueIVMD5/IssueIVMD5Test.java | 42 + .../hutool/json/issueIVMD5/StudentInfo.java | 28 + .../java/cn/hutool/json/test/bean/ADT.java | 16 + .../java/cn/hutool/json/test/bean/Data.java | 14 + .../java/cn/hutool/json/test/bean/Exam.java | 67 + .../hutool/json/test/bean/ExamInfoDict.java | 63 + .../cn/hutool/json/test/bean/JSONBean.java | 22 + .../cn/hutool/json/test/bean/JsonNode.java | 49 + .../hutool/json/test/bean/JsonRootBean.java | 61 + .../cn/hutool/json/test/bean/KeyBean.java | 24 + .../bean/PerfectEvaluationProductResVo.java | 965 ++++ .../java/cn/hutool/json/test/bean/Price.java | 16 + .../hutool/json/test/bean/ProductResBase.java | 71 + .../cn/hutool/json/test/bean/ResultDto.java | 156 + .../java/cn/hutool/json/test/bean/Seq.java | 25 + .../json/test/bean/TokenAuthResponse.java | 22 + .../hutool/json/test/bean/TokenAuthWarp.java | 24 + .../hutool/json/test/bean/TokenAuthWarp2.java | 9 + .../java/cn/hutool/json/test/bean/UUMap.java | 21 + .../java/cn/hutool/json/test/bean/UserA.java | 40 + .../java/cn/hutool/json/test/bean/UserB.java | 28 + .../java/cn/hutool/json/test/bean/UserC.java | 31 + .../hutool/json/test/bean/UserInfoDict.java | 79 + .../json/test/bean/UserInfoRedundCount.java | 38 + .../cn/hutool/json/test/bean/UserWithMap.java | 15 + .../json/test/bean/report/CaseReport.java | 31 + .../json/test/bean/report/EnvSettingInfo.java | 243 + .../json/test/bean/report/StepReport.java | 160 + .../json/test/bean/report/SuiteReport.java | 31 + .../src/test/resources/evaluation.json | 126 + hutool-json/src/test/resources/exam_test.json | 281 ++ .../src/test/resources/issueIVMD5.json | 81 + .../src/test/resources/suiteReport.json | 115 + hutool-log/pom.xml | 81 + .../main/java/cn/hutool/log/AbstractLog.java | 148 + .../java/cn/hutool/log/GlobalLogFactory.java | 76 + .../src/main/java/cn/hutool/log/Log.java | 58 + .../main/java/cn/hutool/log/LogFactory.java | 263 ++ .../main/java/cn/hutool/log/StaticLog.java | 244 + .../log/dialect/commons/ApacheCommonsLog.java | 146 + .../commons/ApacheCommonsLog4JLog.java | 28 + .../commons/ApacheCommonsLogFactory.java | 42 + .../log/dialect/commons/package-info.java | 7 + .../log/dialect/console/ConsoleLog.java | 140 + .../dialect/console/ConsoleLogFactory.java | 27 + .../log/dialect/console/package-info.java | 7 + .../cn/hutool/log/dialect/jboss/JbossLog.java | 141 + .../log/dialect/jboss/JbossLogFactory.java | 32 + .../log/dialect/jboss/package-info.java | 7 + .../cn/hutool/log/dialect/jdk/JdkLog.java | 166 + .../hutool/log/dialect/jdk/JdkLogFactory.java | 59 + .../hutool/log/dialect/jdk/package-info.java | 7 + .../cn/hutool/log/dialect/log4j/Log4jLog.java | 120 + .../log/dialect/log4j/Log4jLogFactory.java | 28 + .../log/dialect/log4j/package-info.java | 7 + .../hutool/log/dialect/log4j2/Log4j2Log.java | 147 + .../log/dialect/log4j2/Log4j2LogFactory.java | 28 + .../log/dialect/log4j2/package-info.java | 7 + .../cn/hutool/log/dialect/package-info.java | 7 + .../cn/hutool/log/dialect/slf4j/Slf4jLog.java | 181 + .../log/dialect/slf4j/Slf4jLogFactory.java | 75 + .../log/dialect/slf4j/package-info.java | 7 + .../hutool/log/dialect/tinylog/TinyLog.java | 170 + .../log/dialect/tinylog/TinyLogFactory.java | 32 + .../log/dialect/tinylog/package-info.java | 7 + .../java/cn/hutool/log/level/DebugLog.java | 47 + .../java/cn/hutool/log/level/ErrorLog.java | 47 + .../java/cn/hutool/log/level/InfoLog.java | 47 + .../main/java/cn/hutool/log/level/Level.java | 41 + .../java/cn/hutool/log/level/TraceLog.java | 47 + .../java/cn/hutool/log/level/WarnLog.java | 47 + .../cn/hutool/log/level/package-info.java | 7 + .../main/java/cn/hutool/log/package-info.java | 7 + .../cn/hutool/log/test/CustomLogTest.java | 105 + .../test/java/cn/hutool/log/test/LogTest.java | 40 + .../cn/hutool/log/test/StaticLogTest.java | 13 + .../src/test/resources/example/log4j2.xml | 72 + .../src/test/resources/log4j.properties | 6 + hutool-log/src/test/resources/log4j2.xml | 24 + hutool-log/src/test/resources/logback.xml | 17 + .../src/test/resources/logging.properties | 39 + .../src/test/resources/tinylog.properties | 11 + hutool-poi/pom.xml | 52 + .../main/java/cn/hutool/poi/PoiChecker.java | 27 + .../cn/hutool/poi/excel/BigExcelWriter.java | 127 + .../java/cn/hutool/poi/excel/ExcelBase.java | 320 ++ .../cn/hutool/poi/excel/ExcelFileUtil.java | 54 + .../cn/hutool/poi/excel/ExcelPicUtil.java | 109 + .../java/cn/hutool/poi/excel/ExcelReader.java | 482 ++ .../java/cn/hutool/poi/excel/ExcelUtil.java | 588 +++ .../java/cn/hutool/poi/excel/ExcelWriter.java | 977 ++++ .../java/cn/hutool/poi/excel/RowUtil.java | 85 + .../java/cn/hutool/poi/excel/StyleSet.java | 187 + .../cn/hutool/poi/excel/WorkbookUtil.java | 287 ++ .../cn/hutool/poi/excel/cell/CellEditor.java | 18 + .../cn/hutool/poi/excel/cell/CellUtil.java | 285 ++ .../cn/hutool/poi/excel/cell/CellValue.java | 17 + .../poi/excel/cell/FormulaCellValue.java | 23 + .../hutool/poi/excel/cell/package-info.java | 6 + .../poi/excel/editors/NumericToIntEditor.java | 22 + .../hutool/poi/excel/editors/TrimEditor.java | 23 + .../poi/excel/editors/package-info.java | 6 + .../cn/hutool/poi/excel/package-info.java | 7 + .../poi/excel/sax/AbstractExcelSaxReader.java | 38 + .../cn/hutool/poi/excel/sax/CellDataType.java | 73 + .../poi/excel/sax/Excel03SaxReader.java | 302 ++ .../poi/excel/sax/Excel07SaxReader.java | 381 ++ .../hutool/poi/excel/sax/ExcelSaxReader.java | 72 + .../cn/hutool/poi/excel/sax/ExcelSaxUtil.java | 159 + .../poi/excel/sax/handler/RowHandler.java | 19 + .../poi/excel/sax/handler/package-info.java | 7 + .../cn/hutool/poi/excel/sax/package-info.java | 7 + .../java/cn/hutool/poi/excel/style/Align.java | 11 + .../cn/hutool/poi/excel/style/StyleUtil.java | 178 + .../hutool/poi/excel/style/package-info.java | 7 + .../hutool/poi/exceptions/POIException.java | 32 + .../hutool/poi/exceptions/package-info.java | 7 + .../main/java/cn/hutool/poi/package-info.java | 9 + .../main/java/cn/hutool/poi/word/DocUtil.java | 37 + .../java/cn/hutool/poi/word/TableUtil.java | 153 + .../java/cn/hutool/poi/word/Word07Writer.java | 220 + .../java/cn/hutool/poi/word/WordUtil.java | 30 + .../java/cn/hutool/poi/word/package-info.java | 7 + .../poi/excel/test/BigExcelWriteTest.java | 209 + .../hutool/poi/excel/test/CellUtilTest.java | 19 + .../hutool/poi/excel/test/ExcelReadTest.java | 193 + .../poi/excel/test/ExcelSaxReadTest.java | 107 + .../hutool/poi/excel/test/ExcelUtilTest.java | 39 + .../hutool/poi/excel/test/ExcelWriteTest.java | 368 ++ .../cn/hutool/poi/excel/test/TestBean.java | 51 + .../hutool/poi/word/test/WordWriterTest.java | 24 + hutool-poi/src/test/resources/aaa.xls | Bin 0 -> 25088 bytes hutool-poi/src/test/resources/aaa.xlsx | Bin 0 -> 11285 bytes hutool-poi/src/test/resources/alias.xlsx | Bin 0 -> 8961 bytes .../src/test/resources/blankAndDateTest.xlsx | Bin 0 -> 10055 bytes hutool-poi/src/test/resources/priceIndex.xls | Bin 0 -> 22016 bytes hutool-script/pom.xml | 24 + .../script/FullSupportScriptEngine.java | 159 + .../cn/hutool/script/JavaScriptEngine.java | 136 + .../hutool/script/ScriptRuntimeException.java | 127 + .../java/cn/hutool/script/ScriptUtil.java | 119 + .../java/cn/hutool/script/package-info.java | 7 + .../cn/hutool/script/test/ScriptUtilTest.java | 33 + hutool-setting/pom.xml | 29 + .../java/cn/hutool/setting/AbsSetting.java | 293 ++ .../java/cn/hutool/setting/GroupedMap.java | 321 ++ .../java/cn/hutool/setting/GroupedSet.java | 315 ++ .../main/java/cn/hutool/setting/Setting.java | 693 +++ .../java/cn/hutool/setting/SettingLoader.java | 225 + .../setting/SettingRuntimeException.java | 31 + .../java/cn/hutool/setting/SettingUtil.java | 46 + .../java/cn/hutool/setting/dialect/Props.java | 514 ++ .../hutool/setting/dialect/package-info.java | 7 + .../java/cn/hutool/setting/package-info.java | 7 + .../hutool/setting/profile/GlobalProfile.java | 36 + .../cn/hutool/setting/profile/Profile.java | 148 + .../hutool/setting/profile/package-info.java | 7 + .../cn/hutool/setting/test/PropsTest.java | 44 + .../cn/hutool/setting/test/SettingTest.java | 58 + .../hutool/setting/test/SettingUtilTest.java | 15 + .../src/test/resources/example/example.set | 24 + .../test/resources/example/example.setting | 20 + .../resources/example/group-set-example.set | 13 + .../src/test/resources/test.properties | 13 + .../src/test/resources/test.setting | 16 + hutool-socket/pom.xml | 31 + .../java/cn/hutool/socket/SocketConfig.java | 117 + .../hutool/socket/SocketRuntimeException.java | 33 + .../java/cn/hutool/socket/SocketUtil.java | 46 + .../cn/hutool/socket/aio/AcceptHandler.java | 37 + .../java/cn/hutool/socket/aio/AioClient.java | 138 + .../java/cn/hutool/socket/aio/AioServer.java | 186 + .../java/cn/hutool/socket/aio/AioSession.java | 206 + .../java/cn/hutool/socket/aio/IoAction.java | 35 + .../cn/hutool/socket/aio/ReadHandler.java | 25 + .../cn/hutool/socket/aio/SimpleIoAction.java | 24 + .../cn/hutool/socket/aio/package-info.java | 7 + .../java/cn/hutool/socket/nio/NioClient.java | 83 + .../java/cn/hutool/socket/nio/NioServer.java | 174 + .../java/cn/hutool/socket/nio/Operation.java | 48 + .../cn/hutool/socket/nio/package-info.java | 7 + .../java/cn/hutool/socket/package-info.java | 7 + .../cn/hutool/socket/protocol/MsgDecoder.java | 24 + .../cn/hutool/socket/protocol/MsgEncoder.java | 23 + .../cn/hutool/socket/protocol/Protocol.java | 23 + .../hutool/socket/protocol/package-info.java | 7 + .../java/cn/hutool/socket/AioClientTest.java | 31 + .../java/cn/hutool/socket/AioServerTest.java | 45 + hutool-system/pom.xml | 24 + .../main/java/cn/hutool/system/HostInfo.java | 75 + .../main/java/cn/hutool/system/JavaInfo.java | 393 ++ .../cn/hutool/system/JavaRuntimeInfo.java | 225 + .../java/cn/hutool/system/JavaSpecInfo.java | 74 + .../main/java/cn/hutool/system/JvmInfo.java | 89 + .../java/cn/hutool/system/JvmSpecInfo.java | 73 + .../main/java/cn/hutool/system/OsInfo.java | 441 ++ .../java/cn/hutool/system/RuntimeInfo.java | 68 + .../java/cn/hutool/system/SystemUtil.java | 472 ++ .../main/java/cn/hutool/system/UserInfo.java | 125 + .../java/cn/hutool/system/package-info.java | 7 + .../java/cn/hutool/system/SystemUtilTest.java | 33 + hutool.sh | 32 + pom.xml | 184 + 1215 files changed, 159913 insertions(+) create mode 100644 .gitee/ISSUE_TEMPLATE.zh-CN.md create mode 100644 .gitee/PULL_REQUEST_TEMPLATE.zh-CN.md create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 CHANGELOG.md create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100755 bin/check_dependency_updates.sh create mode 100755 bin/commit.sh create mode 100755 bin/deploy.sh create mode 100755 bin/install.sh create mode 100755 bin/javadoc.sh create mode 100755 bin/logo.sh create mode 100755 bin/push_dev.sh create mode 100755 bin/push_master.sh create mode 100755 bin/replaceVersion.sh create mode 100755 bin/test.sh create mode 100755 bin/update_version.sh create mode 100755 bin/version.txt create mode 100644 docs/.nojekyll create mode 100644 docs/docs/index.html create mode 100644 docs/index.html create mode 100644 docs/js/version.js create mode 100644 hutool-all/pom.xml create mode 100644 hutool-all/src/main/java/cn/hutool/Hutool.java create mode 100644 hutool-all/src/main/java/cn/hutool/package-info.java create mode 100644 hutool-aop/pom.xml create mode 100644 hutool-aop/src/main/java/cn/hutool/aop/ProxyUtil.java create mode 100644 hutool-aop/src/main/java/cn/hutool/aop/aspects/Aspect.java create mode 100644 hutool-aop/src/main/java/cn/hutool/aop/aspects/SimpleAspect.java create mode 100644 hutool-aop/src/main/java/cn/hutool/aop/aspects/TimeIntervalAspect.java create mode 100644 hutool-aop/src/main/java/cn/hutool/aop/aspects/package-info.java create mode 100644 hutool-aop/src/main/java/cn/hutool/aop/interceptor/CglibInterceptor.java create mode 100644 hutool-aop/src/main/java/cn/hutool/aop/interceptor/JdkInterceptor.java create mode 100644 hutool-aop/src/main/java/cn/hutool/aop/interceptor/package-info.java create mode 100644 hutool-aop/src/main/java/cn/hutool/aop/package-info.java create mode 100644 hutool-aop/src/main/java/cn/hutool/aop/proxy/CglibProxyFactory.java create mode 100644 hutool-aop/src/main/java/cn/hutool/aop/proxy/JdkProxyFactory.java create mode 100644 hutool-aop/src/main/java/cn/hutool/aop/proxy/ProxyFactory.java create mode 100644 hutool-aop/src/main/java/cn/hutool/aop/proxy/package-info.java create mode 100644 hutool-aop/src/test/java/cn/hutool/aop/test/AopTest.java create mode 100644 hutool-bloomFilter/pom.xml create mode 100644 hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BitMapBloomFilter.java create mode 100644 hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BitSetBloomFilter.java create mode 100644 hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BloomFilter.java create mode 100644 hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BloomFilterUtil.java create mode 100644 hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/BitMap.java create mode 100644 hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/IntMap.java create mode 100644 hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/LongMap.java create mode 100644 hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/package-info.java create mode 100644 hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/AbstractFilter.java create mode 100644 hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/DefaultFilter.java create mode 100644 hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/ELFFilter.java create mode 100644 hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/FNVFilter.java create mode 100644 hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/HfFilter.java create mode 100644 hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/HfIpFilter.java create mode 100644 hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/JSFilter.java create mode 100644 hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/PJWFilter.java create mode 100644 hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/RSFilter.java create mode 100644 hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/SDBMFilter.java create mode 100644 hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/TianlFilter.java create mode 100644 hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/package-info.java create mode 100644 hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/package-info.java create mode 100644 hutool-bloomFilter/src/test/java/cn/hutool/bloomfilter/BitMapBloomFilterTest.java create mode 100644 hutool-bom/pom.xml create mode 100644 hutool-cache/pom.xml create mode 100644 hutool-cache/src/main/java/cn/hutool/cache/Cache.java create mode 100644 hutool-cache/src/main/java/cn/hutool/cache/CacheUtil.java create mode 100644 hutool-cache/src/main/java/cn/hutool/cache/GlobalPruneTimer.java create mode 100644 hutool-cache/src/main/java/cn/hutool/cache/file/AbstractFileCache.java create mode 100644 hutool-cache/src/main/java/cn/hutool/cache/file/LFUFileCache.java create mode 100644 hutool-cache/src/main/java/cn/hutool/cache/file/LRUFileCache.java create mode 100644 hutool-cache/src/main/java/cn/hutool/cache/file/package-info.java create mode 100644 hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java create mode 100644 hutool-cache/src/main/java/cn/hutool/cache/impl/CacheObj.java create mode 100644 hutool-cache/src/main/java/cn/hutool/cache/impl/CacheObjIterator.java create mode 100644 hutool-cache/src/main/java/cn/hutool/cache/impl/CacheValuesIterator.java create mode 100644 hutool-cache/src/main/java/cn/hutool/cache/impl/FIFOCache.java create mode 100644 hutool-cache/src/main/java/cn/hutool/cache/impl/LFUCache.java create mode 100644 hutool-cache/src/main/java/cn/hutool/cache/impl/LRUCache.java create mode 100644 hutool-cache/src/main/java/cn/hutool/cache/impl/NoCache.java create mode 100644 hutool-cache/src/main/java/cn/hutool/cache/impl/TimedCache.java create mode 100644 hutool-cache/src/main/java/cn/hutool/cache/impl/WeakCache.java create mode 100644 hutool-cache/src/main/java/cn/hutool/cache/impl/package-info.java create mode 100644 hutool-cache/src/main/java/cn/hutool/cache/package-info.java create mode 100644 hutool-cache/src/test/java/cn/hutool/cache/test/CacheConcurrentTest.java create mode 100644 hutool-cache/src/test/java/cn/hutool/cache/test/CacheTest.java create mode 100644 hutool-cache/src/test/java/cn/hutool/cache/test/FileCacheTest.java create mode 100644 hutool-captcha/pom.xml create mode 100644 hutool-captcha/src/main/java/cn/hutool/captcha/AbstractCaptcha.java create mode 100644 hutool-captcha/src/main/java/cn/hutool/captcha/CaptchaUtil.java create mode 100644 hutool-captcha/src/main/java/cn/hutool/captcha/CircleCaptcha.java create mode 100644 hutool-captcha/src/main/java/cn/hutool/captcha/ICaptcha.java create mode 100644 hutool-captcha/src/main/java/cn/hutool/captcha/LineCaptcha.java create mode 100644 hutool-captcha/src/main/java/cn/hutool/captcha/ShearCaptcha.java create mode 100644 hutool-captcha/src/main/java/cn/hutool/captcha/generator/AbstractGenerator.java create mode 100644 hutool-captcha/src/main/java/cn/hutool/captcha/generator/CodeGenerator.java create mode 100644 hutool-captcha/src/main/java/cn/hutool/captcha/generator/MathGenerator.java create mode 100644 hutool-captcha/src/main/java/cn/hutool/captcha/generator/RandomGenerator.java create mode 100644 hutool-captcha/src/main/java/cn/hutool/captcha/generator/package-info.java create mode 100644 hutool-captcha/src/main/java/cn/hutool/captcha/package-info.java create mode 100644 hutool-captcha/src/test/java/cn/hutool/captcha/CaptchaTest.java create mode 100644 hutool-captcha/src/test/java/cn/hutool/captcha/CaptchaUtilTest.java create mode 100644 hutool-core/pom.xml create mode 100644 hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/annotation/CombinationAnnotationElement.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/annotation/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/bean/BeanDesc.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/bean/BeanDescCache.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/bean/BeanException.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/bean/BeanInfoCache.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/bean/BeanPath.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/bean/BeanUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/bean/DynaBean.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanCopier.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/bean/copier/CopyOptions.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/bean/copier/ValueProvider.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/bean/copier/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/bean/copier/provider/BeanValueProvider.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/bean/copier/provider/MapValueProvider.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/bean/copier/provider/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/bean/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/builder/Builder.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/builder/CompareToBuilder.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/builder/EqualsBuilder.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/builder/HashCodeBuilder.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/builder/IDKey.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/builder/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/clone/CloneRuntimeException.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/clone/CloneSupport.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/clone/Cloneable.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/clone/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/codec/BCD.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/codec/Base32.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/codec/Base62.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/codec/Base62Codec.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/codec/Base64.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/codec/Base64Decoder.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/codec/Base64Encoder.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/codec/Caesar.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/codec/Morse.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/codec/Rot.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/codec/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/collection/ArrayIter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/collection/BoundedPriorityQueue.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/collection/CollectionUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/collection/ConcurrentHashSet.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/collection/CopiedIter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/collection/EnumerationIter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/collection/IterUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/collection/IteratorEnumeration.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/collection/LineIter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/collection/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/comparator/ComparableComparator.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/comparator/ComparatorChain.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/comparator/ComparatorException.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/comparator/CompareUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/comparator/FieldComparator.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/comparator/IndexedComparator.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/comparator/PinyinComparator.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/comparator/PropertyComparator.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/comparator/ReverseComparator.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/comparator/VersionComparator.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/comparator/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/AbstractConverter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/BasicType.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/Convert.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/ConvertException.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/Converter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/ConverterRegistry.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/NumberChineseFormater.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/NumberWordFormater.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/impl/ArrayConverter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/impl/AtomicBooleanConverter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/impl/AtomicReferenceConverter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/impl/BeanConverter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/impl/BooleanConverter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/impl/CalendarConverter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/impl/CastConverter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/impl/CharacterConverter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/impl/CharsetConverter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/impl/ClassConverter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/impl/CollectionConverter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/impl/CurrencyConverter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/impl/DateConverter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/impl/EnumConverter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/impl/GenericEnumConverter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/impl/Jdk8DateConverter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/impl/LocaleConverter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/impl/MapConverter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/impl/NumberConverter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/impl/PathConverter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/impl/PrimitiveConverter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/impl/ReferenceConverter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/impl/StackTraceElementConverter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/impl/StringConverter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/impl/TimeZoneConverter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/impl/URIConverter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/impl/URLConverter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/impl/UUIDConverter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/impl/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/convert/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/date/BetweenFormater.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/date/DateBetween.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/date/DateException.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/date/DateField.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/date/DateModifier.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/date/DatePattern.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/date/DateRange.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/date/DateTime.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/date/DateUnit.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/date/Month.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/date/Quarter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/date/Season.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/date/SystemClock.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/date/TimeInterval.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/date/Week.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/date/Zodiac.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/date/format/AbstractDateBasic.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/date/format/DateBasic.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/date/format/DateParser.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/date/format/DatePrinter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/date/format/FastDateFormat.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/date/format/FastDateParser.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/date/format/FastDatePrinter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/date/format/FormatCache.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/date/format/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/date/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/exceptions/DependencyException.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/exceptions/ExceptionUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/exceptions/NotInitedException.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/exceptions/StatefulException.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/exceptions/UtilException.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/exceptions/ValidateException.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/exceptions/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/getter/ArrayTypeGetter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/getter/BasicTypeGetter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/getter/GroupedTypeGetter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/getter/ListTypeGetter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/getter/OptArrayTypeGetter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/getter/OptBasicTypeGetter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/getter/OptNullBasicTypeFromObjectGetter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/getter/OptNullBasicTypeFromStringGetter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/getter/OptNullBasicTypeGetter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/getter/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/img/GraphicsUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/img/Img.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/img/ImgUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/img/ScaleType.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/img/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/BOMInputStream.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/BufferUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/FastByteArrayOutputStream.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/FastByteBuffer.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/FileTypeUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/FileUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/IORuntimeException.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/IoUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/LineHandler.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/NullOutputStream.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/StreamProgress.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/checksum/CRC16.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/checksum/CRC8.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/checksum/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/file/FileAppender.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/file/FileCopier.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/file/FileMode.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/file/FileReader.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/file/FileWrapper.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/file/FileWriter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/file/LineReadWatcher.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/file/LineSeparator.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/file/Tailer.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/file/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/resource/BytesResource.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/resource/ClassPathResource.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/resource/FileResource.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/resource/InputStreamResource.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/resource/MultiFileResource.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/resource/MultiResource.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/resource/NoResourceException.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/resource/Resource.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/resource/ResourceUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/resource/StringResource.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/resource/UrlResource.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/resource/WebAppResource.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/resource/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/watch/SimpleWatcher.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/watch/WatchException.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/watch/WatchMonitor.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/watch/WatchUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/watch/Watcher.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/watch/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/watch/watchers/DelayWatcher.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/watch/watchers/IgnoreWatcher.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/watch/watchers/WatcherChain.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/io/watch/watchers/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/Assert.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/Chain.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/ClassScaner.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/ConsistentHash.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/Console.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/Dict.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/Editor.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/Filter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/Holder.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/JarClassLoader.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/Matcher.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/MurmurHash.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/ObjectId.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/Pair.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/ParameterizedTypeImpl.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/PatternPool.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/Range.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/Replacer.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/SimpleCache.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/Singleton.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/Snowflake.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/Tuple.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/TypeReference.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/UUID.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/Validator.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/WeightRandom.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/caller/Caller.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/caller/CallerUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/caller/SecurityManagerCaller.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/caller/StackTraceCaller.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/caller/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/copier/Copier.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/copier/SrcToDestCopier.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/copier/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/func/Func.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/func/Func0.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/func/Func1.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/func/VoidFunc.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/func/VoidFunc0.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/func/VoidFunc1.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/func/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/loader/AtomicLoader.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/loader/LazyLoader.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/loader/Loader.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/loader/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/mutable/Mutable.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableBool.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableByte.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableDouble.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableFloat.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableInt.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableLong.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableObj.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableShort.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/mutable/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/lang/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/map/CamelCaseLinkedMap.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/map/CamelCaseMap.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/map/CaseInsensitiveLinkedMap.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/map/CaseInsensitiveMap.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/map/CustomKeyMap.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/map/FixedLinkedHashMap.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/map/MapBuilder.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/map/MapProxy.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/map/MapUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/map/MapWrapper.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/map/TableMap.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/map/multi/CollectionValueMap.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/map/multi/ListValueMap.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/map/multi/SetValueMap.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/map/multi/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/map/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/math/Arrangement.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/math/Combination.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/math/MathUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/math/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/net/LocalPortGenerater.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/net/NetUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/net/URLEncoder.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/net/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/swing/DesktopUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/swing/RobotUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/swing/ScreenUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/swing/clipboard/ClipboardListener.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/swing/clipboard/ClipboardMonitor.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/swing/clipboard/ClipboardUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/swing/clipboard/ImageSelection.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/swing/clipboard/StrClipboardListener.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/swing/clipboard/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/swing/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/text/ASCIIStrCache.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/text/Simhash.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/text/StrBuilder.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/text/StrFormatter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/text/StrSpliter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/text/TextSimilarity.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/text/UnicodeUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/text/csv/CsvConfig.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/text/csv/CsvData.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/text/csv/CsvParser.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/text/csv/CsvReadConfig.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/text/csv/CsvReader.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/text/csv/CsvRow.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/text/csv/CsvUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/text/csv/CsvWriteConfig.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/text/csv/CsvWriter.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/text/csv/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/text/escape/Html4Escape.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/text/escape/Html4Unescape.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/text/escape/InternalEscapeUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/text/escape/NumericEntityUnescaper.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/text/escape/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/text/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/text/replacer/LookupReplacer.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/text/replacer/ReplacerChain.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/text/replacer/StrReplacer.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/text/replacer/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/thread/ConcurrencyTester.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/thread/ExecutorBuilder.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/thread/GlobalThreadPool.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/thread/NamedThreadFactory.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/thread/RejectPolicy.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/thread/SemaphoreRunnable.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/thread/SyncFinisher.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/thread/ThreadFactoryBuilder.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/thread/ThreadUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/thread/lock/NoLock.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/thread/lock/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/thread/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/thread/threadlocal/NamedInheritableThreadLocal.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/thread/threadlocal/NamedThreadLocal.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/thread/threadlocal/package-info.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/util/ArrayUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/util/BooleanUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/util/CharUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/util/CharsetUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/util/ClassLoaderUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/util/ClassUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/util/EnumUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/util/EscapeUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/util/HashUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/util/HexUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/util/IdUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/util/IdcardUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/util/ImageUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/util/ModifierUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/util/NumberUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/util/ObjectUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/util/PageUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/util/PinyinUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/util/RandomUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/util/ReUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/util/ReferenceUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/util/ReflectUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/util/RuntimeUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/util/StrUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/util/TypeUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/util/URLUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/util/XmlUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/util/ZipUtil.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/util/package-info.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/annotation/AnnotationForTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/annotation/AnnotationUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/bean/BeanDescTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/bean/BeanPathTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/bean/BeanUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/bean/DynaBeanTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/clone/CloneTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/codec/BCDTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/codec/Base32Test.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/codec/Base62Test.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/codec/Base64Test.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/codec/CaesarTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/codec/MorseTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/codec/RotTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/collection/CollUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/collection/IterUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/collection/MapProxyTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/comparator/VersionComparatorTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/convert/ConvertOtherTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/convert/ConvertTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/convert/ConvertToArrayTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/convert/ConvertToBeanTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/convert/ConvertToCollectionTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/convert/ConvertToSBCAndDBCTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/convert/ConverterRegistryTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/convert/MapConvertTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/convert/NumberChineseFormaterTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/convert/NumberWordFormatTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/date/BetweenFormaterTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/date/DateBetweenTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/date/DateFieldTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/date/DateModifierTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/date/DateTimeTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/date/DateUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/date/TimeZoneTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/date/ZodiacTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/exceptions/ExceptionUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/img/ImgTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/img/ImgUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/io/BufferUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/io/ClassPathResourceTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/io/FileCopierTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/io/FileReaderTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/io/FileTypeUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/io/FileUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/io/WatchMonitorTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/io/checksum/CrcTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/io/file/TailerTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/lang/AssertTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/lang/CallerTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/lang/ClassScanerTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/lang/ConsoleTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/lang/DictTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/lang/ObjectIdTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/lang/RangeTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/lang/SimhashTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/lang/SnowflakeTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/lang/StrFormatterTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/lang/StrSpliterTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/lang/TextSimilarityTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/lang/ValidatorTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/lang/WeightRandomTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/lang/test/bean/ExamInfoDict.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/lang/test/bean/UserInfoDict.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/lang/test/bean/UserInfoRedundCount.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/map/CamelCaseMapTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/map/CaseInsensitiveMapTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/map/MapUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/math/ArrangementTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/math/CombinationTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/net/NetUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/swing/ClipboardMonitorTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/swing/ClipboardUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/swing/DesktopUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/swing/RobotUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/text/StrBuilderTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/text/UnicodeUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/text/csv/CsvParserTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/text/csv/CsvReaderTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/text/csv/CsvUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/thread/ConcurrencyTesterTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/thread/ThreadUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/util/ArrayUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/util/BooleanUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/util/CharUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/util/ClassLoaderUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/util/ClassUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/util/EnumUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/util/EscapeUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/util/HexUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/util/IdUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/util/IdcardUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/util/NumberUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/util/ObjectUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/util/PageUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/util/PinyinUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/util/RandomUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/util/ReUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/util/ReflectUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/util/RuntimeUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/util/StrUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/util/TypeUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/util/URLUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/util/XmlUtilTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/util/ZipUtilTest.java create mode 100644 hutool-core/src/test/resources/hutool.jpg create mode 100644 hutool-core/src/test/resources/test.csv create mode 100644 hutool-core/src/test/resources/test.properties create mode 100644 hutool-core/src/test/resources/test.xml create mode 100644 hutool-cron/pom.xml create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/CronException.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/CronTimer.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/CronUtil.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/Scheduler.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/TaskExecutor.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/TaskExecutorManager.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/TaskLauncher.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/TaskLauncherManager.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/TaskTable.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/listener/SimpleTaskListener.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/listener/TaskListener.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/listener/TaskListenerManager.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/listener/package-info.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/package-info.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/pattern/CronPattern.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/pattern/CronPatternUtil.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/AlwaysTrueValueMatcher.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/BoolArrayValueMatcher.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/DayOfMonthValueMatcher.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/ValueMatcher.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/ValueMatcherBuilder.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/YearValueMatcher.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/package-info.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/pattern/package-info.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/DayOfMonthValueParser.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/DayOfWeekValueParser.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/HourValueParser.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/MinuteValueParser.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/MonthValueParser.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/SecondValueParser.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/SimpleValueParser.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/ValueParser.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/YearValueParser.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/package-info.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/task/InvokeTask.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/task/RunnableTask.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/task/Task.java create mode 100644 hutool-cron/src/main/java/cn/hutool/cron/task/package-info.java create mode 100644 hutool-cron/src/test/java/cn/hutool/cron/demo/AddAndRemoveMainTest.java create mode 100644 hutool-cron/src/test/java/cn/hutool/cron/demo/CronTest.java create mode 100644 hutool-cron/src/test/java/cn/hutool/cron/demo/DeamonMainTest.java create mode 100644 hutool-cron/src/test/java/cn/hutool/cron/demo/JobMainTest.java create mode 100644 hutool-cron/src/test/java/cn/hutool/cron/demo/TestJob.java create mode 100644 hutool-cron/src/test/java/cn/hutool/cron/demo/TestJob2.java create mode 100644 hutool-cron/src/test/java/cn/hutool/cron/pattern/CronPatternTest.java create mode 100644 hutool-cron/src/test/java/cn/hutool/cron/pattern/CronPatternUtilTest.java create mode 100644 hutool-cron/src/test/resources/config/cron.setting create mode 100644 hutool-crypto/pom.xml create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/BCUtil.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/CryptoException.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/GlobalBouncyCastleProvider.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/KeyUtil.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/Mode.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/Padding.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/ProviderFactory.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/SecureUtil.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/SmUtil.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/AbstractAsymmetricCrypto.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/AsymmetricAlgorithm.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/AsymmetricCrypto.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/BaseAsymmetric.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/KeyType.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/RSA.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/SM2.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/SM2Engine.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/Sign.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/SignAlgorithm.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/package-info.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/digest/BCrypt.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/digest/DigestAlgorithm.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/digest/DigestUtil.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/digest/Digester.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/digest/HMac.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/digest/HmacAlgorithm.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/digest/MD5.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/digest/mac/BCHMacEngine.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/digest/mac/DefaultHMacEngine.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/digest/mac/MacEngine.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/digest/mac/MacEngineFactory.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/digest/mac/package-info.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/digest/package-info.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/package-info.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/AES.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/DES.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/DESede.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/RC4.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/SymmetricAlgorithm.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/SymmetricCrypto.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/Vigenere.java create mode 100644 hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/package-info.java create mode 100644 hutool-crypto/src/test/java/cn/hutool/crypto/test/BCUtilTest.java create mode 100644 hutool-crypto/src/test/java/cn/hutool/crypto/test/HmacTest.java create mode 100644 hutool-crypto/src/test/java/cn/hutool/crypto/test/KeyUtilTest.java create mode 100644 hutool-crypto/src/test/java/cn/hutool/crypto/test/RC4Test.java create mode 100644 hutool-crypto/src/test/java/cn/hutool/crypto/test/RSATest.java create mode 100644 hutool-crypto/src/test/java/cn/hutool/crypto/test/SM2Test.java create mode 100644 hutool-crypto/src/test/java/cn/hutool/crypto/test/SignTest.java create mode 100644 hutool-crypto/src/test/java/cn/hutool/crypto/test/SmTest.java create mode 100644 hutool-crypto/src/test/java/cn/hutool/crypto/test/SymmetricTest.java create mode 100644 hutool-crypto/src/test/java/cn/hutool/crypto/test/digest/DigestTest.java create mode 100644 hutool-crypto/src/test/java/cn/hutool/crypto/test/digest/Md5Test.java create mode 100644 hutool-crypto/src/test/resources/test_private_key.pem create mode 100644 hutool-crypto/src/test/resources/test_public_key.csr create mode 100644 hutool-db/pom.xml create mode 100644 hutool-db/src/main/java/cn/hutool/db/AbstractDb.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/ActiveEntity.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/DaoTemplate.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/Db.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/DbRuntimeException.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/DbUtil.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/Entity.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/Page.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/PageResult.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/Session.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/SqlConnRunner.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/SqlRunner.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/StatementUtil.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/ThreadLocalConnection.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/dialect/Dialect.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/dialect/DialectFactory.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/dialect/DialectName.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/dialect/DriverUtil.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/dialect/impl/AnsiSqlDialect.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/dialect/impl/H2Dialect.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/dialect/impl/MysqlDialect.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/dialect/impl/OracleDialect.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/dialect/impl/PostgresqlDialect.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/dialect/impl/SqlServer2012Dialect.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/dialect/impl/Sqlite3Dialect.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/dialect/impl/package-info.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/dialect/package-info.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/ds/AbstractDSFactory.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/ds/DSFactory.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/ds/DataSourceWrapper.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/ds/GlobalDSFactory.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/ds/c3p0/C3p0DSFactory.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/ds/c3p0/package-info.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/ds/dbcp/DbcpDSFactory.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/ds/dbcp/package-info.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/ds/druid/DruidDSFactory.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/ds/druid/package-info.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/ds/hikari/HikariDSFactory.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/ds/hikari/package-info.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/ds/jndi/JndiDSFactory.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/ds/jndi/package-info.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/ds/package-info.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/ds/pooled/ConnectionWraper.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/ds/pooled/DbConfig.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/ds/pooled/DbSetting.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/ds/pooled/PooledConnection.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/ds/pooled/PooledDSFactory.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/ds/pooled/PooledDataSource.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/ds/pooled/package-info.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/ds/simple/AbstractDataSource.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/ds/simple/SimpleDSFactory.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/ds/simple/SimpleDataSource.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/ds/simple/package-info.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/ds/tomcat/TomcatDSFactory.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/ds/tomcat/package-info.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/handler/BeanHandler.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/handler/BeanListHandler.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/handler/EntityHandler.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/handler/EntityListHandler.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/handler/EntitySetHandler.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/handler/HandleHelper.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/handler/NumberHandler.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/handler/PageResultHandler.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/handler/RsHandler.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/handler/StringHandler.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/handler/package-info.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/meta/Column.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/meta/JdbcType.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/meta/MetaUtil.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/meta/Table.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/meta/TableType.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/meta/package-info.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/nosql/mongo/MongoDS.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/nosql/mongo/MongoFactory.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/nosql/mongo/package-info.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/nosql/package-info.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/nosql/redis/RedisDS.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/nosql/redis/package-info.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/package-info.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/sql/Condition.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/sql/Direction.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/sql/LogicalOperator.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/sql/NamedSql.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/sql/Order.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/sql/Query.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/sql/SqlBuilder.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/sql/SqlExecutor.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/sql/SqlFormatter.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/sql/SqlLog.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/sql/SqlUtil.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/sql/StatementWrapper.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/sql/Wrapper.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/sql/package-info.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/transaction/TransactionLevel.java create mode 100644 hutool-db/src/main/java/cn/hutool/db/transaction/package-info.java create mode 100644 hutool-db/src/test/java/cn/hutool/db/CRUDTest.java create mode 100644 hutool-db/src/test/java/cn/hutool/db/ConcurentTest.java create mode 100644 hutool-db/src/test/java/cn/hutool/db/DbTest.java create mode 100644 hutool-db/src/test/java/cn/hutool/db/DsTest.java create mode 100644 hutool-db/src/test/java/cn/hutool/db/EntityTest.java create mode 100644 hutool-db/src/test/java/cn/hutool/db/FindBeanTest.java create mode 100644 hutool-db/src/test/java/cn/hutool/db/HsqldbTest.java create mode 100644 hutool-db/src/test/java/cn/hutool/db/MySQLTest.java create mode 100644 hutool-db/src/test/java/cn/hutool/db/NamedSqlTest.java create mode 100644 hutool-db/src/test/java/cn/hutool/db/OracleTest.java create mode 100644 hutool-db/src/test/java/cn/hutool/db/PostgreTest.java create mode 100644 hutool-db/src/test/java/cn/hutool/db/SessionTest.java create mode 100644 hutool-db/src/test/java/cn/hutool/db/SqlServerTest.java create mode 100644 hutool-db/src/test/java/cn/hutool/db/UpdateTest.java create mode 100644 hutool-db/src/test/java/cn/hutool/db/meta/MetaUtilTest.java create mode 100644 hutool-db/src/test/java/cn/hutool/db/pojo/User.java create mode 100644 hutool-db/src/test/java/cn/hutool/db/sql/ConditionTest.java create mode 100644 hutool-db/src/test/java/cn/hutool/db/sql/SqlBuilderTest.java create mode 100644 hutool-db/src/test/resources/config/db.setting create mode 100644 hutool-db/src/test/resources/config/example/db-example-c3p0.setting create mode 100644 hutool-db/src/test/resources/config/example/db-example-dbcp.setting create mode 100644 hutool-db/src/test/resources/config/example/db-example-druid.setting create mode 100644 hutool-db/src/test/resources/config/example/db-example-hikari.setting create mode 100644 hutool-db/src/test/resources/config/example/db-example-tomcat.setting create mode 100644 hutool-db/src/test/resources/config/example/mongo-example.setting create mode 100644 hutool-db/src/test/resources/config/redis.setting create mode 100644 hutool-db/src/test/resources/logback.xml create mode 100644 hutool-db/src/test/resources/simplelogger.properties create mode 100644 hutool-db/test.db create mode 100644 hutool-dfa/pom.xml create mode 100644 hutool-dfa/src/main/java/cn/hutool/dfa/SensitiveUtil.java create mode 100644 hutool-dfa/src/main/java/cn/hutool/dfa/StopChar.java create mode 100644 hutool-dfa/src/main/java/cn/hutool/dfa/WordTree.java create mode 100644 hutool-dfa/src/main/java/cn/hutool/dfa/package-info.java create mode 100644 hutool-dfa/src/test/java/cn/hutool/dfa/test/DfaTest.java create mode 100644 hutool-extra/pom.xml create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/emoji/EmojiUtil.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/emoji/package-info.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/ftp/AbstractFtp.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/ftp/Ftp.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/ftp/FtpException.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/ftp/FtpMode.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/ftp/package-info.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/mail/GlobalMailAccount.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/mail/InternalMailUtil.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/mail/Mail.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/mail/MailAccount.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/mail/MailException.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/mail/MailUtil.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/mail/UserPassAuthenticator.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/mail/package-info.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/package-info.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/qrcode/BufferedImageLuminanceSource.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/qrcode/QrCodeException.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/qrcode/QrCodeUtil.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/qrcode/QrConfig.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/qrcode/package-info.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/servlet/ServletUtil.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/servlet/multipart/MultipartFormData.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/servlet/multipart/MultipartRequestInputStream.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/servlet/multipart/UploadFile.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/servlet/multipart/UploadFileHeader.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/servlet/multipart/UploadSetting.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/servlet/multipart/package-info.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/servlet/package-info.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/ssh/ChannelType.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/ssh/Connector.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/ssh/JschRuntimeException.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/ssh/JschSessionPool.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/ssh/JschUtil.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/ssh/Sftp.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/ssh/package-info.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/template/AbstractTemplate.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/template/Template.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/template/TemplateConfig.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/template/TemplateEngine.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/template/TemplateException.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/template/TemplateUtil.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/template/engine/TemplateFactory.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/template/engine/beetl/BeetlEngine.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/template/engine/beetl/BeetlTemplate.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/template/engine/beetl/BeetlUtil.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/template/engine/beetl/package-info.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/template/engine/enjoy/EnjoyEngine.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/template/engine/enjoy/EnjoyTemplate.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/template/engine/enjoy/package-info.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/template/engine/freemarker/FreemarkerEngine.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/template/engine/freemarker/FreemarkerTemplate.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/template/engine/freemarker/SimpleStringTemplateLoader.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/template/engine/freemarker/package-info.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/template/engine/package-info.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/template/engine/rythm/RythmEngine.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/template/engine/rythm/RythmTemplate.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/template/engine/rythm/package-info.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/template/engine/thymeleaf/ThymeleafEngine.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/template/engine/thymeleaf/ThymeleafTemplate.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/template/engine/thymeleaf/package-info.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/template/engine/velocity/SimpleStringResourceLoader.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/template/engine/velocity/VelocityEngine.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/template/engine/velocity/VelocityTemplate.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/template/engine/velocity/VelocityUtil.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/template/engine/velocity/package-info.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/template/package-info.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/AbstractResult.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/Result.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/TokenizerEngine.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/TokenizerException.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/TokenizerUtil.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/Word.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/TokenizerFactory.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/analysis/AnalysisEngine.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/analysis/AnalysisResult.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/analysis/AnalysisWord.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/analysis/SmartcnEngine.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/analysis/package-info.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ansj/AnsjEngine.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ansj/AnsjResult.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ansj/AnsjWord.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ansj/package-info.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/hanlp/HanLPEngine.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/hanlp/HanLPResult.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/hanlp/HanLPWord.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/hanlp/package-info.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ikanalyzer/IKAnalyzerEngine.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ikanalyzer/IKAnalyzerResult.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ikanalyzer/IKAnalyzerWord.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ikanalyzer/package-info.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jcseg/JcsegEngine.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jcseg/JcsegResult.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jcseg/JcsegWord.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jcseg/package-info.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jieba/JiebaEngine.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jieba/JiebaResult.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jieba/JiebaWord.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jieba/package-info.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/mmseg/MmsegEngine.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/mmseg/MmsegResult.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/mmseg/MmsegWord.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/mmseg/package-info.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/package-info.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/word/WordEngine.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/word/WordResult.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/word/WordWord.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/word/package-info.java create mode 100644 hutool-extra/src/main/java/cn/hutool/extra/tokenizer/package-info.java create mode 100644 hutool-extra/src/test/java/cn/hutool/extra/emoji/EmojiUtilTest.java create mode 100644 hutool-extra/src/test/java/cn/hutool/extra/ftp/FtpTest.java create mode 100644 hutool-extra/src/test/java/cn/hutool/extra/mail/MailAccountTest.java create mode 100644 hutool-extra/src/test/java/cn/hutool/extra/mail/MailTest.java create mode 100644 hutool-extra/src/test/java/cn/hutool/extra/qrcode/QrCodeUtilTest.java create mode 100644 hutool-extra/src/test/java/cn/hutool/extra/ssh/JschUtilTest.java create mode 100644 hutool-extra/src/test/java/cn/hutool/extra/template/BeetlUtilTest.java create mode 100644 hutool-extra/src/test/java/cn/hutool/extra/template/TemplateUtilTest.java create mode 100644 hutool-extra/src/test/java/cn/hutool/extra/template/ThymeleafTest.java create mode 100644 hutool-extra/src/test/java/cn/hutool/extra/tokenizer/TokenizerUtilTest.java create mode 100644 hutool-extra/src/test/resources/beetl.properties create mode 100644 hutool-extra/src/test/resources/config/mail.setting create mode 100644 hutool-extra/src/test/resources/example/beetl-example.properties create mode 100644 hutool-extra/src/test/resources/example/mail-example.setting create mode 100644 hutool-extra/src/test/resources/example/velocity-example.vm create mode 100644 hutool-extra/src/test/resources/templates/beetl_test.btl create mode 100644 hutool-extra/src/test/resources/templates/enjoy_test.etl create mode 100644 hutool-extra/src/test/resources/templates/freemarker_test.ftl create mode 100644 hutool-extra/src/test/resources/templates/rythm_test.tmpl create mode 100644 hutool-extra/src/test/resources/templates/thymeleaf_test.ttl create mode 100644 hutool-extra/src/test/resources/templates/velocity_test.vtl create mode 100644 hutool-http/pom.xml create mode 100644 hutool-http/src/main/java/cn/hutool/http/ContentType.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/GlobalHeaders.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/HTMLFilter.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/Header.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/HtmlUtil.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/HttpBase.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/HttpConnection.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/HttpException.java create mode 100755 hutool-http/src/main/java/cn/hutool/http/HttpGlobalConfig.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/HttpRequest.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/HttpResponse.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/HttpStatus.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/HttpUtil.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/Method.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/Status.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/cookie/GlobalCookieManager.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/cookie/ThreadLocalCookieStore.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/cookie/package-info.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/package-info.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/ssl/AndroidSupportSSLFactory.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/ssl/CustomProtocolsSSLFactory.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/ssl/DefaultTrustManager.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/ssl/SSLSocketFactoryBuilder.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/ssl/TrustAnyHostnameVerifier.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/ssl/package-info.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/useragent/Browser.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/useragent/Engine.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/useragent/OS.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/useragent/Platform.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/useragent/UserAgent.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/useragent/UserAgentInfo.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/useragent/UserAgentParser.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/useragent/UserAgentUtil.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/useragent/package-info.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/webservice/SoapClient.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/webservice/SoapProtocol.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/webservice/SoapRuntimeException.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/webservice/SoapUtil.java create mode 100644 hutool-http/src/main/java/cn/hutool/http/webservice/package-info.java create mode 100644 hutool-http/src/test/java/cn/hutool/http/test/DownloadTest.java create mode 100644 hutool-http/src/test/java/cn/hutool/http/test/HtmlUtilTest.java create mode 100644 hutool-http/src/test/java/cn/hutool/http/test/HttpRequestTest.java create mode 100644 hutool-http/src/test/java/cn/hutool/http/test/HttpUtilTest.java create mode 100644 hutool-http/src/test/java/cn/hutool/http/test/RestTest.java create mode 100644 hutool-http/src/test/java/cn/hutool/http/test/UploadTest.java create mode 100644 hutool-http/src/test/java/cn/hutool/http/useragent/UserAgentUtilTest.java create mode 100644 hutool-http/src/test/java/cn/hutool/http/webservice/SoapClientTest.java create mode 100644 hutool-json/pom.xml create mode 100644 hutool-json/src/main/java/cn/hutool/json/InternalJSONUtil.java create mode 100644 hutool-json/src/main/java/cn/hutool/json/JSON.java create mode 100644 hutool-json/src/main/java/cn/hutool/json/JSONArray.java create mode 100644 hutool-json/src/main/java/cn/hutool/json/JSONConfig.java create mode 100644 hutool-json/src/main/java/cn/hutool/json/JSONConverter.java create mode 100644 hutool-json/src/main/java/cn/hutool/json/JSONException.java create mode 100644 hutool-json/src/main/java/cn/hutool/json/JSONGetter.java create mode 100644 hutool-json/src/main/java/cn/hutool/json/JSONNull.java create mode 100644 hutool-json/src/main/java/cn/hutool/json/JSONObject.java create mode 100644 hutool-json/src/main/java/cn/hutool/json/JSONObjectIter.java create mode 100644 hutool-json/src/main/java/cn/hutool/json/JSONStrFormater.java create mode 100644 hutool-json/src/main/java/cn/hutool/json/JSONString.java create mode 100644 hutool-json/src/main/java/cn/hutool/json/JSONSupport.java create mode 100644 hutool-json/src/main/java/cn/hutool/json/JSONTokener.java create mode 100644 hutool-json/src/main/java/cn/hutool/json/JSONUtil.java create mode 100644 hutool-json/src/main/java/cn/hutool/json/XML.java create mode 100644 hutool-json/src/main/java/cn/hutool/json/XMLTokener.java create mode 100644 hutool-json/src/main/java/cn/hutool/json/package-info.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/JSONArrayTest.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/JSONConvertTest.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/JSONObjectTest.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/JSONPathTest.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/JSONStrFormaterTest.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/JSONUtilTest.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/ParseBeanTest.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/issueIVMD5/BaseResult.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/issueIVMD5/IssueIVMD5Test.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/issueIVMD5/StudentInfo.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/test/bean/ADT.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/test/bean/Data.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/test/bean/Exam.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/test/bean/ExamInfoDict.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/test/bean/JSONBean.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/test/bean/JsonNode.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/test/bean/JsonRootBean.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/test/bean/KeyBean.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/test/bean/PerfectEvaluationProductResVo.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/test/bean/Price.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/test/bean/ProductResBase.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/test/bean/ResultDto.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/test/bean/Seq.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/test/bean/TokenAuthResponse.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/test/bean/TokenAuthWarp.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/test/bean/TokenAuthWarp2.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/test/bean/UUMap.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/test/bean/UserA.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/test/bean/UserB.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/test/bean/UserC.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/test/bean/UserInfoDict.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/test/bean/UserInfoRedundCount.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/test/bean/UserWithMap.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/test/bean/report/CaseReport.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/test/bean/report/EnvSettingInfo.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/test/bean/report/StepReport.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/test/bean/report/SuiteReport.java create mode 100644 hutool-json/src/test/resources/evaluation.json create mode 100644 hutool-json/src/test/resources/exam_test.json create mode 100644 hutool-json/src/test/resources/issueIVMD5.json create mode 100644 hutool-json/src/test/resources/suiteReport.json create mode 100644 hutool-log/pom.xml create mode 100644 hutool-log/src/main/java/cn/hutool/log/AbstractLog.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/GlobalLogFactory.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/Log.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/LogFactory.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/StaticLog.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/dialect/commons/ApacheCommonsLog.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/dialect/commons/ApacheCommonsLog4JLog.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/dialect/commons/ApacheCommonsLogFactory.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/dialect/commons/package-info.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/dialect/console/ConsoleLog.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/dialect/console/ConsoleLogFactory.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/dialect/console/package-info.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/dialect/jboss/JbossLog.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/dialect/jboss/JbossLogFactory.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/dialect/jboss/package-info.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/dialect/jdk/JdkLog.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/dialect/jdk/JdkLogFactory.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/dialect/jdk/package-info.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/dialect/log4j/Log4jLog.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/dialect/log4j/Log4jLogFactory.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/dialect/log4j/package-info.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/dialect/log4j2/Log4j2Log.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/dialect/log4j2/Log4j2LogFactory.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/dialect/log4j2/package-info.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/dialect/package-info.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/dialect/slf4j/Slf4jLog.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/dialect/slf4j/Slf4jLogFactory.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/dialect/slf4j/package-info.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/dialect/tinylog/TinyLog.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/dialect/tinylog/TinyLogFactory.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/dialect/tinylog/package-info.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/level/DebugLog.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/level/ErrorLog.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/level/InfoLog.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/level/Level.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/level/TraceLog.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/level/WarnLog.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/level/package-info.java create mode 100644 hutool-log/src/main/java/cn/hutool/log/package-info.java create mode 100644 hutool-log/src/test/java/cn/hutool/log/test/CustomLogTest.java create mode 100644 hutool-log/src/test/java/cn/hutool/log/test/LogTest.java create mode 100644 hutool-log/src/test/java/cn/hutool/log/test/StaticLogTest.java create mode 100644 hutool-log/src/test/resources/example/log4j2.xml create mode 100644 hutool-log/src/test/resources/log4j.properties create mode 100644 hutool-log/src/test/resources/log4j2.xml create mode 100644 hutool-log/src/test/resources/logback.xml create mode 100644 hutool-log/src/test/resources/logging.properties create mode 100644 hutool-log/src/test/resources/tinylog.properties create mode 100644 hutool-poi/pom.xml create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/PoiChecker.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/excel/BigExcelWriter.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelBase.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelFileUtil.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelPicUtil.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelReader.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelUtil.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelWriter.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/excel/RowUtil.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/excel/StyleSet.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/excel/WorkbookUtil.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/excel/cell/CellEditor.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/excel/cell/CellUtil.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/excel/cell/CellValue.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/excel/cell/FormulaCellValue.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/excel/cell/package-info.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/excel/editors/NumericToIntEditor.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/excel/editors/TrimEditor.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/excel/editors/package-info.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/excel/package-info.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/excel/sax/AbstractExcelSaxReader.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/excel/sax/CellDataType.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel03SaxReader.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel07SaxReader.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/excel/sax/ExcelSaxReader.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/excel/sax/ExcelSaxUtil.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/excel/sax/handler/RowHandler.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/excel/sax/handler/package-info.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/excel/sax/package-info.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/excel/style/Align.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/excel/style/StyleUtil.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/excel/style/package-info.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/exceptions/POIException.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/exceptions/package-info.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/package-info.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/word/DocUtil.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/word/TableUtil.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/word/Word07Writer.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/word/WordUtil.java create mode 100644 hutool-poi/src/main/java/cn/hutool/poi/word/package-info.java create mode 100644 hutool-poi/src/test/java/cn/hutool/poi/excel/test/BigExcelWriteTest.java create mode 100644 hutool-poi/src/test/java/cn/hutool/poi/excel/test/CellUtilTest.java create mode 100644 hutool-poi/src/test/java/cn/hutool/poi/excel/test/ExcelReadTest.java create mode 100644 hutool-poi/src/test/java/cn/hutool/poi/excel/test/ExcelSaxReadTest.java create mode 100644 hutool-poi/src/test/java/cn/hutool/poi/excel/test/ExcelUtilTest.java create mode 100644 hutool-poi/src/test/java/cn/hutool/poi/excel/test/ExcelWriteTest.java create mode 100644 hutool-poi/src/test/java/cn/hutool/poi/excel/test/TestBean.java create mode 100644 hutool-poi/src/test/java/cn/hutool/poi/word/test/WordWriterTest.java create mode 100644 hutool-poi/src/test/resources/aaa.xls create mode 100644 hutool-poi/src/test/resources/aaa.xlsx create mode 100644 hutool-poi/src/test/resources/alias.xlsx create mode 100644 hutool-poi/src/test/resources/blankAndDateTest.xlsx create mode 100644 hutool-poi/src/test/resources/priceIndex.xls create mode 100644 hutool-script/pom.xml create mode 100644 hutool-script/src/main/java/cn/hutool/script/FullSupportScriptEngine.java create mode 100644 hutool-script/src/main/java/cn/hutool/script/JavaScriptEngine.java create mode 100644 hutool-script/src/main/java/cn/hutool/script/ScriptRuntimeException.java create mode 100644 hutool-script/src/main/java/cn/hutool/script/ScriptUtil.java create mode 100644 hutool-script/src/main/java/cn/hutool/script/package-info.java create mode 100644 hutool-script/src/test/java/cn/hutool/script/test/ScriptUtilTest.java create mode 100644 hutool-setting/pom.xml create mode 100644 hutool-setting/src/main/java/cn/hutool/setting/AbsSetting.java create mode 100644 hutool-setting/src/main/java/cn/hutool/setting/GroupedMap.java create mode 100644 hutool-setting/src/main/java/cn/hutool/setting/GroupedSet.java create mode 100644 hutool-setting/src/main/java/cn/hutool/setting/Setting.java create mode 100644 hutool-setting/src/main/java/cn/hutool/setting/SettingLoader.java create mode 100644 hutool-setting/src/main/java/cn/hutool/setting/SettingRuntimeException.java create mode 100644 hutool-setting/src/main/java/cn/hutool/setting/SettingUtil.java create mode 100644 hutool-setting/src/main/java/cn/hutool/setting/dialect/Props.java create mode 100644 hutool-setting/src/main/java/cn/hutool/setting/dialect/package-info.java create mode 100644 hutool-setting/src/main/java/cn/hutool/setting/package-info.java create mode 100644 hutool-setting/src/main/java/cn/hutool/setting/profile/GlobalProfile.java create mode 100644 hutool-setting/src/main/java/cn/hutool/setting/profile/Profile.java create mode 100644 hutool-setting/src/main/java/cn/hutool/setting/profile/package-info.java create mode 100644 hutool-setting/src/test/java/cn/hutool/setting/test/PropsTest.java create mode 100644 hutool-setting/src/test/java/cn/hutool/setting/test/SettingTest.java create mode 100644 hutool-setting/src/test/java/cn/hutool/setting/test/SettingUtilTest.java create mode 100644 hutool-setting/src/test/resources/example/example.set create mode 100644 hutool-setting/src/test/resources/example/example.setting create mode 100644 hutool-setting/src/test/resources/example/group-set-example.set create mode 100644 hutool-setting/src/test/resources/test.properties create mode 100644 hutool-setting/src/test/resources/test.setting create mode 100644 hutool-socket/pom.xml create mode 100644 hutool-socket/src/main/java/cn/hutool/socket/SocketConfig.java create mode 100644 hutool-socket/src/main/java/cn/hutool/socket/SocketRuntimeException.java create mode 100644 hutool-socket/src/main/java/cn/hutool/socket/SocketUtil.java create mode 100644 hutool-socket/src/main/java/cn/hutool/socket/aio/AcceptHandler.java create mode 100644 hutool-socket/src/main/java/cn/hutool/socket/aio/AioClient.java create mode 100644 hutool-socket/src/main/java/cn/hutool/socket/aio/AioServer.java create mode 100644 hutool-socket/src/main/java/cn/hutool/socket/aio/AioSession.java create mode 100644 hutool-socket/src/main/java/cn/hutool/socket/aio/IoAction.java create mode 100644 hutool-socket/src/main/java/cn/hutool/socket/aio/ReadHandler.java create mode 100644 hutool-socket/src/main/java/cn/hutool/socket/aio/SimpleIoAction.java create mode 100644 hutool-socket/src/main/java/cn/hutool/socket/aio/package-info.java create mode 100644 hutool-socket/src/main/java/cn/hutool/socket/nio/NioClient.java create mode 100644 hutool-socket/src/main/java/cn/hutool/socket/nio/NioServer.java create mode 100644 hutool-socket/src/main/java/cn/hutool/socket/nio/Operation.java create mode 100644 hutool-socket/src/main/java/cn/hutool/socket/nio/package-info.java create mode 100644 hutool-socket/src/main/java/cn/hutool/socket/package-info.java create mode 100644 hutool-socket/src/main/java/cn/hutool/socket/protocol/MsgDecoder.java create mode 100644 hutool-socket/src/main/java/cn/hutool/socket/protocol/MsgEncoder.java create mode 100644 hutool-socket/src/main/java/cn/hutool/socket/protocol/Protocol.java create mode 100644 hutool-socket/src/main/java/cn/hutool/socket/protocol/package-info.java create mode 100644 hutool-socket/src/test/java/cn/hutool/socket/AioClientTest.java create mode 100644 hutool-socket/src/test/java/cn/hutool/socket/AioServerTest.java create mode 100644 hutool-system/pom.xml create mode 100644 hutool-system/src/main/java/cn/hutool/system/HostInfo.java create mode 100644 hutool-system/src/main/java/cn/hutool/system/JavaInfo.java create mode 100644 hutool-system/src/main/java/cn/hutool/system/JavaRuntimeInfo.java create mode 100644 hutool-system/src/main/java/cn/hutool/system/JavaSpecInfo.java create mode 100644 hutool-system/src/main/java/cn/hutool/system/JvmInfo.java create mode 100644 hutool-system/src/main/java/cn/hutool/system/JvmSpecInfo.java create mode 100644 hutool-system/src/main/java/cn/hutool/system/OsInfo.java create mode 100644 hutool-system/src/main/java/cn/hutool/system/RuntimeInfo.java create mode 100644 hutool-system/src/main/java/cn/hutool/system/SystemUtil.java create mode 100644 hutool-system/src/main/java/cn/hutool/system/UserInfo.java create mode 100644 hutool-system/src/main/java/cn/hutool/system/package-info.java create mode 100644 hutool-system/src/test/java/cn/hutool/system/SystemUtilTest.java create mode 100755 hutool.sh create mode 100644 pom.xml diff --git a/.gitee/ISSUE_TEMPLATE.zh-CN.md b/.gitee/ISSUE_TEMPLATE.zh-CN.md new file mode 100644 index 000000000..68f65c58c --- /dev/null +++ b/.gitee/ISSUE_TEMPLATE.zh-CN.md @@ -0,0 +1,5 @@ +### 使用的JDK版本和Hutool版本 + +### 问题描述(包括截图) + +### 报错信息 \ No newline at end of file diff --git a/.gitee/PULL_REQUEST_TEMPLATE.zh-CN.md b/.gitee/PULL_REQUEST_TEMPLATE.zh-CN.md new file mode 100644 index 000000000..9a5eb979a --- /dev/null +++ b/.gitee/PULL_REQUEST_TEMPLATE.zh-CN.md @@ -0,0 +1,10 @@ +#### 说明 + +1. 请确认你提交的PR是到'v4-dev'分支,否则我会手动修改代码并关闭PR。 +2. 请确认没有更改代码风格(如tab缩进) +3. 新特性添加请确认注释完备,如有必要,请在src/test/java下添加Junit测试用例 + +### 修改描述(包括说明bug修复还是新特性添加) + +1. [bug修复] balabala…… +2. [新特性] balabala…… \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..08aca5ccb --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Eclipse +.project +.classpath +.settings/ + +# Maven +target/ +dependency-reduced-pom.xml +pom.xml.versionsBackup +.factorypath + +# Gradle +.gradle/ +build/ + +#IDEA +# idea ignore +.idea/ +*.ipr +*.iml +*.iws + +# temp ignore +*.log +*.cache +*.diff +*.patch +*.tmp + +# system ignore +.DS_Store +Thumbs.db diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..b408077cf --- /dev/null +++ b/.travis.yml @@ -0,0 +1,24 @@ +language: java + +sudo: false # faster builds + +install: true + +jdk: + - openjdk8 + +notifications: + email: false + +cache: + directories: + - '$HOME/.m2' + +script: + - export TZ=Asia/Shanghai + - mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V + - mvn cobertura:cobertura -Dcobertura.report.format=xml -Dmaven.javadoc.skip.true + +after_success: + - bash <(curl -s https://codecov.io/bash) + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..4b5eb4c70 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,1316 @@ + +# Changelog + +------------------------------------------------------------------------------------------------------------- + +## 4.6.2 + +### 新特性 +* 【core】 Tuple增加支持equals和hashcode(issue#469@Github) +* 【http】 Accept修改默认权重,json优先(issue#472@Github) +* 【http】 增加HttpGlobalConfig(issue#I10DHC@Gitee) +* 【core】 CollUtil.getFieldValues避免空指针(issue#I10FK9@Gitee) +* 【http】 改进HtmlUtil.unescape改为EscapeUtil.unescapeHtml4实现(issue#I10AUY@Gitee) +* 【core】 TextSimilarity改进判断(issue#456@Github) +* 【poi】 ExcelWriter支持下拉列表(issue#476@Github) +* 【core】 强化ExceptionUtil(issue#459@Github) +* 【core】 增强日期工具类(pr#455@Github) +* 【setting】 构造Setting增加默认字符编码 + +### Bug修复 +* 【cache】 修复missCount规则(issue#465@Github) +* 【core】 修复父目录拷贝到子目录导致的递归问题 +* 【crypto】 修复RSA中分段加密计算导致的异常(issue#481@Github) + +------------------------------------------------------------------------------------------------------------- + +## 4.6.0 + +### 新特性 +* 【all】 增加hutool-bom模块,用于可排除的依赖引入 +* 【core】 ResourceUtil增加readBytes方法 +* 【captcha】 更换为逻辑字体 +* 【extra】 Mail增加reply(issue#445@Github) +* 【core】 去掉重复方法(issue#IZQYR@Gitee) +* 【db】 改进结果集转Bean的下划线和驼峰兼容性(issue#IZOPL@Gitee) +* 【system】 增加JavaInfo对新版本java的支持(pr#454@Github) +* 【extra】 增加可选标志位,是否返回当前目录(issue#446@Github) + +### Bug修复 +* 【core】 修复ImgUtil.slice宽高取反问题(issue#438@Github) +* 【crypto】 修复MD516位摘要长度错误问题(issue#IZNPE@Gitee) +* 【core】 修复ImgUtil.hexToColor调用参数问题(issue#449@Github) +* 【http】 修复可能存在的Http请求结束未关闭连接的情况(issue#449@Github) + +------------------------------------------------------------------------------------------------------------- + +## 4.5.18 + +### 新特性 +* 【poi】 增加ExcelUtil.getWriterWithSheet方法(感谢@【长沙】NULL) +* 【core】 EnumUtil和ObjectUtil增加方法(pr#57@Gitee) +* 【core】 EnumUtil增加fromString重载支持默认值(issue#IZFXJ@Gitee) +* 【core】 DateUtil.parse增加Locale对象重载(issue#437@Github) + +### Bug修复 +* 【core】 修复无效的日志打印(issue#IZFW9@Gitee) +* 【core】 修复Validator.isBirthday注释(issue#IZFMG@Gitee) +* 【core】 修复TextSimilarity 的bug(issue#435@Github) +* 【core】 修复Tailer预读取行bug(issue#IZHAT@Gitee) +* 【core】 修复使用slf4j-simple不打印日志问题 + +------------------------------------------------------------------------------------------------------------- + +## 4.5.17 + +### 新特性 +* 【http】 SoapClient增加超时设置(issue#IYQHK@Gitee) +* 【captcha】 修正验证码位置,增加可选文字透明度(issue#421@Github) +* 【poi】 ExcelWriter.setRowHeight增加空指针检查(issue#IYN63@Gitee) +* 【core】 ImgUtil增加copyImage可选背景色(issue#IYX3E@Gitee) +* 【core】 CollUtil.sub方法在空列表时返回空数组而非null(issue#430@Github) +* 【core】 改进本地IP地址获取方法(issue#428@Github) +* 【core】 WatchMonitor增加ClosedWatchServiceException异常处理(issue#427@Github) + +### Bug修复 +* 【crypto】 修复DigestUtil.md5方法的注释(issue#IYQHG@Gitee) +* 【core】 修复MapUtil.newHashMap初始容量问题(issue#IYKJJ@Gitee) +* 【core】 修复HttpUtil.encodeParam多出=问题(issue#IZ3PI@Gitee) +* 【core】 修复Img.scale变形问题(issue#431@Gitee) + +------------------------------------------------------------------------------------------------------------- + +## 4.5.16 + +### 新特性 +* 【cache】 缓存增加get重载(pr#404@Github) +* 【poi】 增加WordUtil +* 【core】 改进fnvHash避免负数(issue#IYDK6@Gitee) +* 【core】 改进BeanCoper逻辑(pr#45@Gitee) +* 【all】 实现必要序列化接口 +* 【db】 Entity增加可选忽略大小写(issue#IYGVW@Gitee) +* 【core】 MapUtil增加renameKey方法(感谢@【帝都】宁静) + +### Bug修复 +* 【poi】 修复sax中读取Excel普通单元格设置日期格式识别问题(issue#IYD0L@Gitee) +* 【http】 修复setParam非String值失效问题(issue#IYF9Y@Gitee) +* 【core】 修复FileUtil.cleanEmpty第二层直接删除文件夹的问题(感谢@【上海】风景) + +------------------------------------------------------------------------------------------------------------- + +## 4.5.15 + +### 新特性 + +### Bug修复 +* 【extra】 修复JschUtil.exec不执行命名的问题(issue#405@Github) +* 【http】 修复CookieManager全局设定导致的可能存在的冲突,增加自定义的GlobalCookieManager + +------------------------------------------------------------------------------------------------------------- + +## 4.5.14 + +### 新特性 +* 【poi】 增加TableUtil +* 【http】 HttpRequest增加setCookieManager方法 +* 【http】 改进url错误时的报错信息(感谢@【北京】thumb) + +### Bug修复 +* 【core】 修复ZipUtil.zlib压缩识别问题(感谢@【上海】 沙漏) +* 【log】 调整log模块层次结构,兼容slf4j的API(issue#IY8DX@Gitee) +* 【core】 Convert.toXXX带默认值换成convertQuietly实现,避免异常(issue#403@Gitee) +* 【log】 解决行号错误问题 +* 【log】 修复decimalFormatMoney中整数丢失问题(issue#IY9OV@Gitee) + +------------------------------------------------------------------------------------------------------------- + +## 4.5.13 + +### 新特性 +* 【crypto】 提供HmacSM3支持(issue#396@Github) +* 【setting】 SettingLoader添加同步锁(issue#396@Github) + +### Bug修复 +* 【log】 修复log模块模板拼接时没有判断等级关闭与否的问题 + +------------------------------------------------------------------------------------------------------------- + +## 4.5.12 + +### 新特性 +* 【json】 解析JSON字符串去除两边空白符(同时解决字符串中bom问题(issue#381@Github) +* 【poi】 Sax解析增加在异常后关闭文件的逻辑(issue#IXBOU@Gitee) +* 【core】 MapUtil增加get重载(TypeReference)(issue#IXL81@Gitee) +* 【crypto】 RC4增加encryptHex和encryptBase64方法(issue#387@Github) +* 【core】 DateUtil.parse增加格式(issue#385@Github) +* 【core】 增加CollUtil.containsAny(感谢【北京】宁静) +* 【core】 增加CollUtil.keySet和values(issue#IXYQJ@Gitee) + +### Bug修复 +* 【poi】 解决三目运算符导致类型转换问题(issue#385@Github) +* 【core】 解决NumberUtil.decimalFormatMoney格式错误问题(issue#391@Github) + +------------------------------------------------------------------------------------------------------------- + +## 4.5.11 + +### 新特性 +* 【core】 DateUtil.parse方法识别时间增强(issue#IWMM6@Gitee) +* 【extra】 Mail中Files附件可选为空(issue#365@Github) +* 【extra】 EmojiUtil增加containsEmoji方法(pr#373@Github) +* 【core】 Convert.toDBC()增加空校验(issue#369@Github) + +### Bug修复 +* 【core】 修复NumberUtil.decimalFormatMoney只有整数的bug(issue#IWKVL@Gitee) +* 【bloomFilter】 修复BitMapBloomFilter构造数bug(issue#IWMIN@Gitee) +* 【extra】 MailUtil.send方法传入自定义Setting失效问题(感谢@【上海】康) +* 【core】 修复NetUtil.localIpv4s方法名,改为localIps(issue#IWS2C@Gitee) + +------------------------------------------------------------------------------------------------------------- + +## 4.5.10 + +### 新特性 +* 【extra】 修改MailUtil中的逻辑,默认为非单例邮件客户端(issue#IWFRQ@Gitee) + +### Bug修复 +* 【http】 修复HttpUtil.toParams方法某些符号未转义问题(issue#356@Github) +* 【captcha】 修复验证码被遮挡问题(issue#IWERW@Gitee) +* 【poi】 修复readBySax重复问题(issue#IVKLQ@Gitee) + +------------------------------------------------------------------------------------------------------------- + +## 4.5.9 + +### 新特性 +* 【core】 修改Singleton单例策略,IdUtil增加getSnowflake(issue#IWA0G@Gitee) +* 【core】 增加RandomUtil.randomBoolean(issue#351@Github) +* 【core】 增加Base62实现,Base62类 + +### Bug修复 +* 【json】 修复JSON中含有日期导致的时间戳包含双引号问题 + +------------------------------------------------------------------------------------------------------------- + +## 4.5.8 + +### 新特性 +* 【cron】 CronPatternUtil增加nextDateAfter方法(issue#IVYNL@Github) +* 【core】 增加RandomUtil.randomDate方法(issue#IW49T@Github) +* 【db】 Table增加comment字段,调整元信息逻辑(issue#IW49S@Gitee) +* 【core】 增加ConcurrencyTester(pr#41@Gitee) +* 【core】 ZipUtil增加对流的解压支持(issue#IW798@Gitee) + +### Bug修复 +* 【core】 修复Enjoy模板创建多个引擎报错问题(issue#344@Github) +* 【crypto】 修复Linux下RSA/ECB/PKCS1Padding算法无效问题 +* 【core】 修复ImgUtil.scale方法操作png图片透明失效问题(issue#341@Github) +* 【core】 修复JSON自定义日期格式无引号问题(issue#IW4F6@Gitee) +* 【core】 修复Android下CallerUtil.getCallerCaller空指针问题(issue#IW68U@Gitee) +* 【cache】 修复Cache中超时太大导致Long越界问题(issue#347@Github) + +------------------------------------------------------------------------------------------------------------- + +## 4.5.7 + +### 新特性 +* 【core】 新增StrClipboardListener(issue#325@Github) +* 【core】 新增DesktopUtil(issue#326@Github) +* 【core】 CollUtil.getFieldValues增加可选是否忽略null值(issue#IVGEE@Gitee) +* 【http】 新增SoapUtil,SoapClient支持返回SOAPMessage +* 【core】 RobotUtil增加鼠标相关操作 +* 【core】 增加DateModifier,DateUtil增加truncate和ceiling方法(issue#IVL9A@Gitee) +* 【core】 PageUtil增加getStart(issue#IVN0C@Gitee) +* 【core】 CopyOptions增加ignoreXXX方法(感谢@【南昌】...) +* 【core】 ObjectUtil增加isEmpty方法(感谢@【成都】AliK) + +### Bug修复 +* 【core】 修复PatternPool中的URL_HTTP不支持端口的问题(issue#IVF1V@Gitee) +* 【extra】 修复JschUtil.exec多次connect的问题(issue#339@Github) +* 【http】 修复SoapUtil.toString乱码问题(pr#337@Github) +* 【http】 解决Cookie不规范导致的请求响应失败问题(issue#336@Github) +* 【setting】 GroupedMap增加读写锁解决并发问题(issue#336@Github) +* 【json】 修复JSONArray中add方法导致覆盖问题(感谢@【江门】小草哥) +* 【core】 修复Convert对泛型支持不完善的问题(issue#IVMD5@Gitee) + +------------------------------------------------------------------------------------------------------------- + +## 4.5.6 + +### 新特性 +* 【http】 SoapClient增加setParams,增加构造使用默认的namespaceURI方法 +* 【core】 FileUtil增加cleanEmpty方法(issue#319@Github) +* 【core】 增加ClipboardMonitor(issue#320@Github) +* 【http】 SoapClient增加部分方法 +* 【http】 HttpRequest增加setConnectionTimeout和setReadTimeout(issue#322@Github) +* 【core】 Console增printPrograss +* 【core】 DateBetween增加null校验(issue#IVC23@Gitee) +* 【core】 增加CollUtil.getFieldValues重载(issue#IV96S@Gitee) +* 【db】 SqlExecutor和Db增加executeBatch重载,支持批量SQL(issue#324@Github) + +### Bug修复 +* 【bloomFilter】修复负数导致的问题(issue#IV6X6@Gitee) +* 【setting】 修复Props监听问题 +* 【json】 修复TypeUtil中空指针导致的注入失败问题(issue#IVCLW@Gitee) + +------------------------------------------------------------------------------------------------------------- + +## 4.5.5 + +### 新特性 + +### Bug修复 +* 【core】 Assert中NullPointerException改为IllegalArgumentException(issue#IV41L@Gitee) +* 【core】 修复创建新sheet时比较器未清空导致的顺序问题(issue#318@Github) + +------------------------------------------------------------------------------------------------------------- + +## 4.5.4 + +### 新特性 +* 【core】 NetUtil增加getUsableLocalPort方法,并迁移至cn.hutool.core.net包 +* 【core】 FileUtil增加isSub方法(pr#39@Gitee) +* 【core】 增加VoidFunc +* 【extra】 mail适配mail.setting和config/mail.setting双配置文件(感谢@【江门】小草哥) +* 【corn】 cron适配cron.setting和config/cron.setting双配置文件(感谢@【江门】小草哥) +* 【poi】 ExcelWriter增加autoSizeColumnAll方法,ExcelBase增加getColumnCount、getRowCount方法(感谢@@【长沙】M) +* 【http】 添加SoapClient,删除SoapRequest + +### Bug修复 +* 【db】 修复Session中事务问题(issue#IUQMN@Gitee) +* 【db】 修复Db中关闭逻辑错误导致的事务问题(感谢@【宁波】mojie126) +* 【http】 修复form方法使用Resource可能导致的空指针问题 +* 【crypto】 修复SM2Engine逻辑错误(感谢bcgit/bc-java) + +------------------------------------------------------------------------------------------------------------- + +## 4.5.3 + +### 新特性 +* 【core】 Simhash添加读写锁(issue#IUF9O@Gitee) +* 【core】 Img增加round方法,圆角给定图片 +* 【extra】 二维码中的图片做圆角处理 +* 【core】 CsvData实现Iterable接口 +* 【extra】 Ftp增加重连方法(pr#38@Gitee) +* 【extra】 Velocity升级至2.x,不再兼容1.7 + +### Bug修复 +* 【core】 修复ReflectUtil新建Map对象错误问题(issue#IUF9O@Gitee) +* 【core】 修复ImgUtil字体为null导致的空指针问题(issue#IUF3X@Gitee) +* 【extra】 修复Ftp中文件上传mkdirs方法创建多余文件夹的问题(issue#ITAYV@Gitee) +* 【extra】 修复Ftp中文件上传mkdirs方法创建多余文件夹的问题(issue#ITAYV@Gitee) + +------------------------------------------------------------------------------------------------------------- + +## 4.5.2 + +### 新特性 +* 【crypto】 增加读取pem格式私钥文件和公钥证书的方法,位于BCUtil(issue#ISJ5M@Gitee) +* 【core】 增加StrUtil.byteLength(issue#284@Github) +* 【core】 增加GlobalBouncyCastleProvider,单例使用BouncyCastleProvider +* 【crypto】 增强对BC库的兼容性,明确RSA为RSA/ECB/PKCS1Padding +* 【core】 snowflake生成器添加id反推生成时间等信息的方法(pr#293@Github) +* 【poi】 CellUtil.getCellValue增加null验证 +* 【core】 增加文件内容跟随器Tailer +* 【crypto】 增加RC4算法 +* 【core】 增加FixedLinkedHashMap +* 【extra】 增加ChannelType,JschUtil增加createSession、createChannel、openChannel等方法 +* 【core】 WatchUtil增加createModify +* 【core】 新增ImgUtil,废弃ImageUtil + +### Bug修复 +* 【core】 修复ExceptionUtil(pr#35@Gitee) +* 【core】 修复RandomUtil注释标注问题(pr#288@Github) +* 【core】 修复TimedCache中onRemove失效问题(issue#ITD0O@Gitee) +* 【core】 修复DateConverter日期负数问题(issue#ITWK4@Gitee) +* 【json】 修复toBean时父类定义泛型字段导致的注入问题(issue#ITGGN@Gitee) +* 【cahce】 修复读锁导致的LRU异常(issue#303@Gtihub) +* 【captcha】 修复在某些未知情况下获取字体高度导致的问题 + +------------------------------------------------------------------------------------------------------------- + +## 4.5.1 + +### 新特性 +* 【socket】 socket模块加入到all中 +* 【core】 增加Jdk8DateConverter用于支持jdk8中的时间(issue#IS32N@Gitee) +* 【core】 StrUtil.subPreGbk优化代码规范(pull#277@Github) +* 【crypto】 MD5支持16位值生成 +* 【crypto】 Digester支持自定义盐所在位置 +* 【captcha】 增加算数计算类验证码(issue#282@Github) + +### Bug修复 +* 【json】 修复JSON中toString导致的中文引号被转义问题(感谢@【内蒙】程序员) +* 【core】 修复15位身份证生日校验问题(issue#ISBUO@Gitee) +* 【extra】 修复部分模板引擎classpath路径获取失败问题 + +------------------------------------------------------------------------------------------------------------- + +## 4.5.0 + +### 新特性 +* 【socket】 增加Socket模块 +* 【core】 Validator增加isIpV4方法(issue#IRQ6W@Gitee) +* 【crypto】 增加SM2Engine,支持C1C2C3和C1C3C2两种模式 +* 【core】 StrUtil.splitTrim支持其它空白符(issue#IRVPC@Gitee) +* 【http】 请求支持DELETE附带参数模式(issue#IRW9E@Gitee) +* 【bloomFilter】调整BitMap注释 + +### Bug修复 +* 【crypto】 修复KeyUtil中使用BC库导致的其它密钥生成异常 +* 【core】 修正DateUtil.formatHttpDate方法 +* 【extra】 修复FTP.ls无法遍历文件问题(issue#IRTA3@Gitee) +* 【extra】 修复QrCodeUtil中ratio参数失效问题,调整默认纠错为M(感谢@【上海】皮皮今) +* 【core】 修复FileTypeUtil对jpg文件识别问题(issue#275@Github) +* 【cache】 修复cache使用读锁导致的删除节点并发问题(issue#IRZTL@Gitee) + +------------------------------------------------------------------------------------------------------------- + +## 4.4.5 + +### 新特性 +* 【core】 增加StrFormater代码逻辑可读性(pr#269@Github) +* 【core】 Validator中使用泛型 +* 【core】 NumberUtil增加toBytes和toInt方法 +* 【core】 XmlUtil增加format方法,支持缩进 +* 【http】 SoapRequest增加executeBody方法(issue#IRN6I@Gitee) +* 【core】 调整XmlUtil.toStr方法对编码的逻辑 + +### Bug修复 +* 【core】 修复AnnotationUtil.getAnnotationValue获取对象错误问题(issue#271@Github) + +------------------------------------------------------------------------------------------------------------- + +## 4.4.4 + +### 新特性 +* 【crypto】 增加EC公钥压缩/解压缩(pr#264@Github) +* 【db】 Entity支持IS NOT NULL形式,调整逻辑,强化Condition的toString(issue#267@Github) + +### Bug修复 +* 【core】 修复Profile中路径参数失效问题(issue#265@Github) +* 【core】 修复MapConvert中值类型转换错误的问题(issue#268@Github) + +------------------------------------------------------------------------------------------------------------- + +## 4.4.3 + +### 新特性 +* 【crypto】 MD5以及Digester增加加盐支持(issue#256@Github) +* 【crypto】 整理KeyUtil,减少冗余代码 +* 【core】 增加Zodiac类,DateUtil增加getZodiac、getChineseZodiac用于获取星座和生肖(issue#260@Github) + +### Bug修复 +* 【core】 修复ExceptionUtil.stacktraceToString中limit参数无效问题(issue#IR7UE@Gitee) +* 【core】 修复StrUtil.repeatByLength中数组越界问题(issue#IRB2C@Gitee) +* 【core】 修复FileUtil.remove移动后删除失败问题(issue#IRF8R@Gitee) +* 【extra】 修复Ftp中delDir逻辑导致的问题(issue#IRCQ8@Gitee) +* 【core】 修复XmlUtil.mapToXml中map值为空导致的空指针问题。(issue#IRD7X@Gitee) +* 【poi】 修复ExcelWriter中setOnlyAlias没有排除值的问题。(issue#IRF9L@Gitee) + +------------------------------------------------------------------------------------------------------------- + +## 4.4.2 + +### 新特性 +* 【core】 JSON中添加getStrEscaped方法,并修改原getStr逻辑,不再自动转义(issue#IR7SW@Gitee) +* 【core】 CLassLoaderUtil增加getJarClassLoader和loadClass重载方法(issue#IR94T@Gitee) +* 【crypto】 SM2密钥生成曲线修改为使用sm2p256v1(pr#249@Github) +* 【json】 JSONUtil增加空判断(issue#253@Github) +* 【core】 改进HexUtil.isHexNumber(issue#254@Github) +* 【http】 HttpRequest增加getConnection方法(issue#251@Github) + +### Bug修复 +* 【core】 修复URL转义问题(issue#IR6QP@Gitee) +* 【core】 修复WeightRandom权重为0的对象问题(issue#252@Github) + +------------------------------------------------------------------------------------------------------------- + +## 4.4.1 + +### 新特性 +* 【core】 增加Rot(回转N位简易替换密码)、凯撒密码和莫尔斯电码 +* 【crypto】 增加Vigenere密码 +* 【db】 增加达梦7的驱动识别 +* 【extra】 TemplateEngine适配更广泛的参数类型 +* 【core】 HexUtil增加toHex方法,增加CRC8和CRC16(issue#IQWNB@Gitee) +* 【http】 添加text/xml ContentType(pr#31@Gitee) +* 【core】 Img、ImageUtil增加Resource和Path参数支持 +* 【extra】 ServletUtil.getClientIP增加注释,提示IP伪造风险 +* 【poi】 增加Word07Writer +* 【crypto】 增加KeyUtil,SecureUtil中的密钥生成迁移至此工具类中 +* 【core】 增加URLEncoder(自行实现解决空格转义问题),HttpUtil废弃encode和decode方法 + +### Bug修复 +* 【poi】 解决ExcelWriter中setSheet报错问题(issue#235@Github) +* 【crypto】 解决SecureUtil.readCertificate密码无效问题(issue#240@Github) +* 【json】 修复JSONUtil.toList针对对象中的类无法实例化导致的null问题(issue#239@Github) +* 【db】 修复MongoDS在Single模式下检查配置文件导致的问题(issue#IR2BF@Github) + +------------------------------------------------------------------------------------------------------------- + +## 4.4.0 + +### 新特性 +* 【core】 增加MurmurHash(Murmur3算法实现),HashUtil增加murmur32、murmur64、murmur128方法 +* 【core】 增加Simhash(用于海量文本去重) +* 【extra】 增加分词封装,封装了ansj、HanLP、IKAnalyzer、Jcseg、Jieba、MMSeg、Lucene-analysis、Word的实现,统一了接口 +* 【core】 去除NumberUtil.parseInt和parseLong的8进制支持(issue#234@Github) +* 【extra】 Template部分修改命名减少歧义(Engine->TemplateEngine,EngineFactory->TemplateFactory) +* 【poi】 ExcelWriter中Map支持alias(issue#IQISU@Gitee) + +### Bug修复 + +## 4.3.3 + +### 新特性 +* 【poi】 ExcelWriter增加write重载,可选强制加标题(感谢@【北京】大熊) +* 【core】 ExceptionUtil增加isFromOrSuppressedThrowable(pr#29@Gitee) +* 【core】 ExceptionUtil增加convertFromOrSuppressedThrowable(pr#30@Gitee) +* 【crypto】 非对称和SM2构造传入的私钥和公钥支持Hex和Base64自动识别 + +### Bug修复 +* 【core】 修复padAfter和padPre结果错误问题(issue#IQANO@Gitee) +* 【crypto】 修复SM2签名验证异常(issue#IQAY0@Gitee) +* 【extra】 修复Freemarker字符串模板无效问题(issue#231@Github) +* 【core】 修复StrUtil.strip问题(issue#232@Github) + +------------------------------------------------------------------------------------------------------------- + +## 4.3.2 + +### 新特性 +* 【core】 StrUtil增加equalsAny和equalsAnyIgnoreCase方法(issue#IPUQK@Gitee) +* 【http】 StrUtil增加equalsAny和equalsAnyIgnoreCase方法(issue#223@Github) +* 【http】 StrUtil增加padPre、padAfter、center方法(issue#IPWR0@Gitee) +* 【core】 ImageUtil增加compress方法(issue#IPYIF@Gitee) +* 【core】 ReflectUtil增加getMethodByName、getMethodByNameIgnoreCase(issue#IQ2BO@Gitee) +* 【crypto】 增加SmUtil国密算法工具类(issue#225@Github) +* 【crypto】 增加SM2非对称加密(issue#225@Github) +* 【db】 增加AbstractDSFactory,减少冗余代码 +* 【json】 JSONUtil.toBean增加可选是否忽略错误(issue@227@Gtihub) + +### Bug修复 +* 【core】 修复FileUtil.lastIndexOfSeparator空指针问题(issue#IPXPK@Gitee) +* 【core】 修复ArrayUtil.newArray泛型问题 +* 【core】 修复CsvWriter循环调用问题(issue#IQ8T6@Gitee) +* 【poi】 修复ExcelReader读取Map空头导致的问题(issue#IQ6F2@Gitee) +* 【db】 修复Driver识别导致的SQL Server方言异常(issue#IQ687@Gitee) +* 【core】 修复Number.isInteger和isLong判断问题(issue#229@Github) + +------------------------------------------------------------------------------------------------------------- + +## 4.3.1 + +### 新特性 +* 【core】 新增DateUtil.dateNew方法(issue#217@Github) +* 【extra】 JschUtil.exec增加重载,可选错误输出(issue#IPNAB@Gitee) +* 【core】 增加NoLock(issue#218@Github) +* 【core】 QrCode.decode改进 +* 【core】 合并无必要的构造方法 +* 【setting】 Setting.getMap方法在分组不存在时返回空Map而非null(issue#IPU2X@Gitee) + +### Bug修复 +* 【db】 解决数据源识别错误问题(issue#IPNI7@Gitee) +* 【core】 修复DateField.of缺失字段问题(issue#IPP51@Gitee) +* 【core】 JSONObject中忽略空值失效问题(issue#221@Github) + +------------------------------------------------------------------------------------------------------------- + +## 4.3.0 + +### 新特性 +* 【core】 增加TypeReference类(issue#IPAML@Gitee) +* 【json】 支持TypeReference类转换,并对toBean逻辑做了大量变动(issue#IPAML@Gitee) +* 【core】 ArrayUtil.get和CollUtil.get返回null而非空指针(issue#IPKZO@Gitee) + +### Bug修复 +* 【extra】 修复VelocityEngine中模板中文乱码问题(issue#216@Github) + +------------------------------------------------------------------------------------------------------------- + +## 4.2.2 + +### 新特性 +* 【json】 JSONObject调整构造方法,支持对象转为JSON可选是否有序(issue#IP1Q2@Gitee) +* 【core】 BeanUtil增加hasGetter和hasSetter方法 +* 【core】 StrUtil增加isUperCase和isLowerCase方法,增加removeAll和removeAllLineBreaks(issue#IP7PT@Gitee) +* 【db】 增加PostgreSQL的单元测试 +* 【core】 ArrayUtil增加sub方法泛型支持 +* 【core】 从Apache-commons-lang3移植Builder(issue#IPALY@Gitee) +* 【core】 增加Func1接口,ReUtil和StrUtil增加Func1参数的replace方法(pr#27@Gitee) +* 【db】 Table增加getColumn方法,Column补充注释(issue#209@Github) + +### Bug修复 +* 【cron】 修复L代表的最后一天无效问题(issue#IP5PB@Gitee) +* 【core】 修复验证15位身份证月的判断问题(issue#IP70D@Gitee) +* 【poi】 修复多次调用write方法写出多个标题问题(issue#212@Github) +* 【extra】 修复模板写出文件空白问题(issue#208@Github) + +------------------------------------------------------------------------------------------------------------- + +## 4.2.1 + +### 新特性 +* 【extra】 增加基于emoji-java的EmojiUtil +* 【http】 增加User-agent解析 +* 【crypto】 引入bouncycastle从而对国密SM2、SM3、SM4支持 +* 【poi】 新增ExcelFileUtil,改进错误提示 + +### Bug修复 + +------------------------------------------------------------------------------------------------------------- + +## 4.1.22 + +### 新特性 +* 【core】 BeanUtil.copyProperties方法支持目标为Map(issue#IOQHZ@Gitee) +* 【poi】 ExcelWriter增加方法setOnlyAlias,用于特定字段剔除(issue#IOOVK@Gitee) +* 【captcha】 增加setBackground方法(issue#200@Github) +* 【core】 NetUtil增加idnToASCII方法(issue#201@Github) +* 【log】 增加JBoss-Logging支持(issue#IOVS1@Gitee) +* 【http】 增加URL标准化,从而支持非http开头的URL字符串 + +### Bug修复 +* 【core】 修复Validator.isBirthday + +------------------------------------------------------------------------------------------------------------- + +## 4.1.21 + +### 新特性 +* 【core】 RuntimeUtil增加getErrorResult方法(issue#199@Github) +* 【core】 ReflectUtil增加hasField方法(感谢@【杭州】J辉) +* 【core】 BeanUtil增加toBean方法(感谢@【杭州】J辉) +* 【db】 增加对HSQLDB支持,改进Driver自定识别 + +### Bug修复 +* 【core】 修复EnumUtil.getFieldNames定义name属性重复问题(感谢@【杭州】J辉) +* 【json】 修复List多层嵌套toBean转换失败问题 +* 【core】 修复ObjectUtil.toString问题(issue#IONLA@Gitee) + +------------------------------------------------------------------------------------------------------------- + +## 4.1.20 + +### 新特性 +* 【http】 增强SoapRequest的兼容性(感谢@【南京】陽光) +* 【core】 改进ZipUtil错误提示 +* 【core】 DateUtil.parse方法读取时间时,年月日按照当天计算。(issue#INYCF@Gitee) +* 【core】 DateUtil.parse改进支持UTC时间格式。 +* 【db】 MongoDS支持客户端验证(issue#IO2DS@Gitee) +* 【core】 改进字符串转集合和数组(支持逗号分隔形式)(pr#26@Gitee) +* 【core】 改进DateConverter(issue#IOCWR@Gitee) +* 【core】 改进NumberUtil中转数字,支持字母结尾(issue#IOCWR@Gitee) +* 【poi】 ExcelUtil增加indexToColName和colNameToIndex方法(issue#IO8ZH@Gitee) +* 【core】 Convert.toList修改为泛型(issue#IOJZV@Gitee) +* 【core】 BeanDesc中属性修改为使用LinkedHashMap存储 +* 【core】 ArrayUtil.get和CollUtil.get对于越界返回null而非抛出异常(issue#IOFKL@Gitee) +* 【core】 EnumUtil增加likeValueOf方法(issue#IOFKL@Gitee) +* 【core】 删除CollUtil.sortPageAll2方法,增加ColllUtil.page方法 + +### Bug修复 +* 【core】 修正CollUtil.sortPageAll逻辑(pr#186@Github) +* 【core】 修复ClassLoaderUtil.loadClass不能加载内部类问题(issue#IO4GF@Gitee) +* 【core】 修复CustomKeyLinkedMap继承问题(issue#IO5Y2@Gitee) +* 【core】 修复NumberUtil.isPrimes没有参数校验导致的问题(issue#IO57Q@Gitee) +* 【extra】 修复QrConfig 引入包错误问题(pr#194@Github) +* 【extra】 修复Sftp创建目录问题(issue#INZUP@Gitee) +* 【core】 修复CollUtil.sortPageAll方法 +* 【core】 修复ImageUtil图片旋转出现黑边问题(pr#189@Github) + +------------------------------------------------------------------------------------------------------------- + +## 4.1.19 + +### 新特性 +* 【extra】 Ftp增加setMode方法(issue#INPMZ@Gitee) +* 【core】 IdUtil增加fastUUID和fastSimpleUUID方法(issue#INU37@Gitee) +* 【core】 DateUtil增加formatChineseDate方法(issue#INT6I@Gitee) +* 【core】 ClassUtil中部分方法迁移至ReflectUtil +* 【json】 新增JSONConfig,统一JSON配置,并添加可选的自定义输出日期格式支持 + +### Bug修复 +* 【core】 修复ImageUtil文件流未关闭问题(感谢@【西安】追寻) +* 【core】 修复ZipUtil中gzip和zlib方法未调用finish导致的问题(issue#INSXF@Gitee) +* 【core】 修复ZipUtil中文件目录同名无法压缩的问题(issue#INQ1K@Gitee) + +------------------------------------------------------------------------------------------------------------- + +## 4.1.18 + +### 新特性 +* 【http】 改进字符串匹配正则(issue#INHPD@Gitee) +* 【core】 增加gzip和UnGzip针对流的方法(issue#INKMP@Gitee) +* 【http】 增加ThreadLocalCookieStore + +### Bug修复 +* 【core】 修复BeanUtil.copyProperties参数多余问题 +* 【cron】 修复表达式匹配错误问题(issue#INLEE@Gitee) +* 【core】 修复ReflectUtil获取空参数方法导致的问题(issue#INN5W@Gitee) +* 【json】 修复JSONArray.toList方法导致的问题(issue#INO3F@Gitee) +* 【core】 修复NumberUtil.parseLong中0转换问题方法导致的问题(issue#INO3F@Gitee) +* 【core】 修复CompareUtil循环引用问题(issue#180@Github) + +------------------------------------------------------------------------------------------------------------- + +## 4.1.17 + +### 新特性 + +### Bug修复 +* 【core】 修复JDK7之后比较器中违反自反性导致的问题 +* 【cron】 修改部分逻辑 + +------------------------------------------------------------------------------------------------------------- + +## 4.1.16 + +### 新特性 +* 【core】 Convert.增加boolean类型转数字(issue#INCKM@Gitee) +* 【core】 新增BooleanUtil + +### Bug修复 +* 【core】 修复JDK11下Caller被弃用导致的问题(issue#174@Github) + +------------------------------------------------------------------------------------------------------------- + +## 4.1.15 + +### 新特性 +* 【core】 Convert.toInt增加容错,NumberUtil增加toNumber方法(issue#IN2LP@Gitee) +* 【core】 ImageUtil增加cut切圆形方法(issue#IN3JJ@Gitee) +* 【core】 Img增加setPositionBaseCentre可选坐标计算基于中心(issue#IN3JM@Gitee) +* 【core】 ImageUtil增加逻辑判断颜色模式,避免失色问题(issue#IN3JK@Gitee) +* 【cron】 改进规则支持20/2这类形式 +* 【extra】 ServletUtil.write增加重载方法支持文件(issue#IN9O0@Gitee) + +### Bug修复 +* 【core】 修复DateUtil.yearAndQuarter计算错误的问题(issue#IN38V@Gitee) +* 【core】 修复ClassUtil.isPublic判断问题(issue#IN38V@Gitee) +* 【extra】 修复JschUtil中Session关闭未移除出池导致的问题(issue#171@Github) +* 【core】 修复NumberUtil.isInteger中0判断问题(issue#IN9BS@Gitee) + +------------------------------------------------------------------------------------------------------------- + +## 4.1.14 + +### 新特性 +* 【core】 StrUtil增加hide方法 +* 【core】 PatternPool增加URL_HTTP,原URL规则变更 +* 【extra】 统一FTP和SFTP接口规范 +* 【extra】 QrCodeUtil支持二维码中贴Logo图片 +* 【core】 校准ImageUtil.pressText文字位置 +* 【core】 ImageUtil增加getColor等方法 +* 【core】 增加RobotUtil提供截屏等封装,增加ScreenUtil用于获取屏幕属性 +* 【extra】 QrCodeUtil增加条形码等其它类型支持(issue#IN1CR@Gitee) +* 【core】 增加DateUtil.parseUTC方法(issue#IN1IO@Gitee) +* 【core】 增加DateUtil.isWeekend方法 +* 【all】 加入Travis-CI验证项目构建 + +### Bug修复 +* 【core】 修复ImageUtil.convert转换png变色问题(issue#IMWUO@Gitee) +* 【core】 修复FileUtil.newerThan中null判断的问题(issue#165@Github) +* 【extra】 修复Ftp中mkdir方法引起的数组越界问题 + +------------------------------------------------------------------------------------------------------------- + +## 4.1.13 + +### 新特性 +* 【core】 增加RejectPolicy线程池线程拒绝策略枚举 +* 【core】 DateUtil增加isSame方法 +* 【core】 FileUtil.getAbsolutePath方法在获取不到ClassPath情况下返回原路径 +* 【core】 打印SQL日志覆盖每一个方法 +* 【core】 Convert.toXXX转数字的时候默认去除两边空白符 +* 【poi】 增加BigExcelWriter,支持Excel大数据导出(issue#IK47S@Gitee) +* 【core】 ExceptionUtil增加isCausedBy和getCausedBy方法 +* 【poi】 EnumUtil增加toString和fromString +* 【poi】 新增IdUtil工具类 + +### Bug修复 +* 【core】 修复RuntimeUtil.getResultLines未关闭Process问题(pr#164@Github) +* 【core】 修复ClassPathResource在jar运行模式下的空指针问题 + +------------------------------------------------------------------------------------------------------------- + +## 4.1.12 + +### 新特性 +* 【core】 ExcelReader.read方法返回的Map默认有序 + +### Bug修复 +* 【core】 修复ZipUtil以及FileUtil中slip漏洞(issue#162@Github) +* 【core】 修复ZipUtil路径问题(issue#IMUEK@Gitee) +* 【core】 修复FileUtil.getParent方法获取父路径不严格导致空指针问题 + +------------------------------------------------------------------------------------------------------------- + +## 4.1.11 + +### 新特性 +* 【core】 Convert增加toList方法 +* 【core】 StrUtil增加containsAny针对char的重载 +* 【core】 FileUtil.mainName修正处理逻辑 +* 【core】 CharUtil增加isFileSeparator方法 +* 【core】 增加UUID类,提升Simple模式下性能 +* 【poi】 ExcelUtil增加setStyleSet方法,修改write逻辑,对于单列数据输出,而非忽略(感谢@【宁波】mojie126) +* 【core】 新增WebAppResource类 +* 【extra】 新增Thymeleaf模板支持 +* 【setting】 去除Setting日志 + +### Bug修复 +* 【script】 修复FullSupportScriptEngine构造中ext和mimeType方式获取引擎丢失问题 +* 【cron】 修复定时任务执行阻塞问题 + +------------------------------------------------------------------------------------------------------------- + +## 4.1.10 + +### 新特性 +* 【extra】 Template增加Jfinal的Enjoy模板支持 +* 【core】 Assert增加checkBetween方法,Validator增加isBetween和validatorBetween +* 【core】 增加CollUtil.getLast方法(感谢@【帝都】宁静) +* 【core】 修改Assert.notNull注释(issue#IMI3Z@Gitee) +* 【core】 BeanUtil增加isEmpty和hasNullField方法(pr#157@Github) +* 【log】 ConsoleLog增加setLevel方法(issue#IMLZ3@Gitee) +* 【captcha】 解决验证码超出背景的问题(issue#IHWHE@Gitee) + +### Bug修复 +* 【core】 修复BOMInputStream构造的问题(pr#22@Gitee) +* 【json】 修复toBean中如果字段中为字符串而JSON中为JSONObject对象注入失败问题(issue#IMGBJ@Gitee) +* 【setting】 修复keySet总返回空问题(issue#IMHD7@Gitee) +* 【extra】 修复starttls和SSL连接混淆问题(issue#IMLMD@Gitee) +* 【setting】 修复getStr无法获取默认值问题(issue#IMLMI@Gitee) +* 【core】 修复BeanUtil.mapToBean设置别名失效问题 + +------------------------------------------------------------------------------------------------------------- + +## 4.1.9 + +### 新特性 +* 【core】 MapUtil增加toObjectArray方法 +* 【core】 URLUtil.normalize增加反斜杠处理(issue#IM8BI@Gitee) +* 【core】 增加ClassUtil.getShortClassName(issue#IM8XM@Gitee) +* 【core】 增加ThreadFactoryBuilder和ExecutorBuilder +* 【cron】 定时任务改为线程池实现 +* 【core】 Assert增加checkIndex方法 +* 【core】 parseBoolean增加on、off关键字支持可选字符串 +* 【core】 URLUtil.formatUrl方法兼容更多情况(issue#IMAEA@Gitee) +* 【core】 改进NumberUtil.isInteger和isLong判断(issue#IMDGB@Gitee) +* 【http】 HttpResponse增加isOk方法(issue#155@Github) +* 【http】 改进HttpUtil.downloadXXX方法,返回非2XX抛出异常(issue#IMCTT@Gitee) +* 【http】 HttpRequest增加setUrlHandler方法(issue#IMD1X@Gitee) +* 【http】 HttpRequest增加getCookieManager和closeCookie方法(issue#IMDND@Gitee) + +### Bug修复 +* 【core】 修复IdcardUtil中isValidCard10空指针问题(issue#IMB7R@Gitee) +* 【core】 修复SoapRequest空指针问题(issue#IMBUN@Gitee) +* 【http】 修复文件上传没有关闭File的问题(issue#IMDUY@Gitee) +* 【json】 修复toBean中有Map参数导致的值丢失问题(issue#IMDEM@Gitee) +* 【bloomFilter】修复hash值负数问题(issue#154@Github) +* 【core】 修复Convert中Map强转导致的问题 + +------------------------------------------------------------------------------------------------------------- + +## 4.1.8 + +### 新特性 +* 【http】 HttpRequest增加getUrl、getMethod等方法 +* 【core】 Validator增加isWord和ValidateWord(感谢@【帝都】宁静) +* 【core】 增加CollUtil.filter针对List的重载(issue#IM1NI@Gitee) +* 【core】 增加ImageUtil.toBase64 +* 【http】 增加SoapRequest +* 【poi】 ExcelWriter增加renameSheet方法(issue#150@Github) +* 【core】 ZipUtil增加unzipFileBytes方法(issue#IM5KO@Gitee) +* 【aop】 加入Cglib实现的切面支持(issue#IM4Y2@Gitee) +* 【extra】 加入FTP客户端支持,基于commons-net封装 + +### Bug修复 +* 【http】 修复编码自动识别的bug(issue#IM33O@Gitee) +* 【db】 修复Session中ds引起的空指针问题(感谢@【武汉】jellard) +* 【core】 修复ReflectUtil.newInstance二次调用资源问题(issue#IM51X@Gitee) +* 【core】 修复ClassScaner包名前缀引起的问题(issue#IM5OJ@Gitee) + +------------------------------------------------------------------------------------------------------------- + +## 4.1.7 + +### 新特性 +* 【db】 SqlRunner被弃用 + +### Bug修复 +* 【db】 修复Oracle分页问题(issue#ILZDA@Gitee) +* 【db】 Dialect使用单例 + +------------------------------------------------------------------------------------------------------------- + +## 4.1.6 + +### 新特性 +* 【core】 OptNullBasicTypeGetter增加getDate方法(issue#ILUQM@Gitee) +* 【core】 RuntimeUtil增加可选环境变量参数(issue#ILV2I@Gitee) +* 【core】 修改Caller结构 + +### Bug修复 +* 【db】 修复Oracle分页多一条问题(issue#ILUQM@Gitee) +* 【poi】 修复ExcelWriter换行问题(issue#ILXLI@Gitee) + +------------------------------------------------------------------------------------------------------------- + +## 4.1.5 + +### 新特性 +* 【poi】 ExcelWriter支持通过别名方式设置Bean写出的顺序(感谢@【武汉】zzz) +* 【db】 SQL日志打印扩展到所有SQL(感谢@【河北】理想主义) +* 【core】 增加FileUtil.copyFilesFromDir方法(issue#ILRLG@Gitee) +* 【core】 EscapeUtil.unescapeHtml4和EscapeUtil.escapeHtml4(issue#112@Github) +* 【http】 增加CustomProtocolsSSLFactory和AndroidSupportSSLFactory(pr#142@Github) +* 【setting】 添加SettingUtil(感谢@【杭州】t-io) +* 【bloomFilter】添加BloomFilterUtil +* 【core】 添加Img类 + +### Bug修复 +* 【http】 修复body方法判断Content-Type失效问题(感谢@【上海】皮皮今) +* 【core】 修复FileUtil.copy方法在目标不存在的情况下报错问题 +* 【core】 修复ClassScaner在Spring boot fat jar下扫描失败的问题(issue#IKDJW@Gitee) +* 【json】 修复JSONObject构造names列表为空导致的构造空对象(issue#143@Github ) +* 【core】 修复ImageUtil.pressText图片有黑边的问题(issue#141@Github) + + +------------------------------------------------------------------------------------------------------------- + +## 4.1.4 + +### 新特性 +* 【all】 补充package-info +* 【db】 增加方法SqlExecutor.callQuery(issue#ILJ0N@Gitee) +* 【core】 ExceptionUtil增加部分方法 +* 【system】 SystemUtil增加部分方法 +* 【core】 新增NamedThreadLocal(issue#ILJ0Z@Gitee) +* 【core】 ZipUtil新增Zlib压缩解压 +* 【core】 NumberUtil增加parseInt和parseLong,支持10进制、8进制和16进制自动识别 +* 【db】 Table继承自LinkedHashMap保证字段读出有序(感谢@【帝都】宁静) +* 【json】 JSONObject子类自动判断是否有序(感谢@【帝都】宁静) +* 【poi】 抽象ExcelBase,提取共用方法 + +### Bug修复 +* 【http】 修复HttpRequest.setFollowRedirects无效问题(issue#ILIKG@Gitee) +* 【core】 修复CharUtil.isEmoji问题 +* 【http】 修复HttpResponse.writeBody同步模式下写出失败问题 +* 【http】 修复Cookie机制导致的部分Cookie信息不能在请求时附带的问题 +* 【json】 修复JSONArray.toArray转换为原始类型导致的异常问题 + +------------------------------------------------------------------------------------------------------------- + +## 4.1.3 + +### 新特性 +* 【all】 优化db的DsFactory、log的LogFactory、extra的TemplateUtil逻辑,减少异常栈嵌套 +* 【core】 Validator增加isMac、validateMac方法(感谢@【上海】阳仔) + +### Bug修复 +* 【core】 修复ArrayUtil.join前后fix失效问题(@【河北】理想主义) +* 【core】 修复DateRange最后一个元素逻辑问题(issue#ILE38@Gitee) +* 【cron】 修复调用CronUtil.stop()方法无法正常结束作业进程的问题(issue#ILFCZ@Gitee) +* 【db】 修复page方法在Oracle中丢失参数问题(issue#ILGXP@Gitee) +* 【extra】 修复QrCodeUtil.decode对复杂二维码解码失败问题(感谢@【成都】小朋友) + +------------------------------------------------------------------------------------------------------------- + +## 4.1.2 + +### 新特性 +* 【core】 MapUtil增加getDate方法(感谢@【帝都】宁静) +* 【json】 putByPath方法增加容错性,支持下标越界识别为追加(issue#IKNM6@Gitee) +* 【core】 增加FileUtil.getParent方法(pr#18@Gitee) +* 【core】 ImageUtil.pressText增加抗锯齿(pr#19@Gitee) +* 【core】 BeanUtil.getPropertyDescriptors去除class属性(issue#IKVKR@Gitee) +* 【json】 putByPath方法针对空的规则变更(issue#IKX2H@Gitee) +* 【captcha】 增加CodeGenerator,可自定义验证码文字生成策略(issue#IL3YH@Gitee) +* 【core】 增加CollUtil.list方法,更灵活的创建ArrayList和LinkedList +* 【core】 DateTime增加时区支持(issue#131@Github) +* 【extra】 QrCodeUtil二维码生成支持设置边距、颜色等自定义项(issue#135@Github) + +### Bug修复 +* 【core】 修复JSONUtil.formatJsonStr引号换行问题(issue#IKMMK@Gitee) +* 【core】 修复URLUtil.getDecodedPath可能导致的空指针问题(issue#IKLRD@Gitee) +* 【core】 修复PinyinUtil.getAllFirstLetter非汉字显示问题(issue#IKM0P@Gitee) +* 【json】 修复当Bean为私有类时无法实例化导致的JSON转换问题(感谢@【上海】风景) +* 【json】 修复Bean中有Object字段时toBean产生的问题(感谢@【上海】风景) +* 【core】 修复XmlUtil关闭XXE避免XXE攻击 +* 【poi】 修复Excel03SaxReader读取小数的问题(感谢@【深圳】rm -rf /) +* 【core】 修复CollUtil.findOne空参数导致的空指针问题(issue#133@Github) +* 【core】 修复JSONArray.addAll问题(pr#137@Github) +* 【core】 修复UnicodeUtil单独空格无法转换问题 + +------------------------------------------------------------------------------------------------------------- + +## 4.1.1 + +### 新特性 +* 【poi】 ExcelWriter写出bean使用LinkedHashMap +* 【core】 UnicodeUtil新增:1、\u大小写不区分,2、\u后跟非16进制按照非Unicode符对待,直接输出(issue#IKJGU@Gitee) +* 【crypto】 增加Bcrypt实现(参照:jBCrypt) +* 【core】 XXXIterator修改为XXXIter,同时实现Iterator和Iterable接口 +* 【core】 Dict使用LinkedHashMap,Entity也是 + +### Bug修复 +* 【setting】 修复store方法无换行问题 +* 【core】 修复UnicodeUtil.toString方法不正确Unicode死循环问题(issue#IKJGU@Gitee) +* 【http】 修复HttpsURLConnectionOLDImpl导致的转换异常(issue#IKKGF@Gitee) +* 【crypto】 修复RSA分段加密解密的bug(感谢@【深圳】Demo) +* 【poi】 修复ExcelWriter写出文件无法覆盖问题(感谢@【宁波】mojie126) +* 【poi】 修复sax方式读取空行空指针问题(issue#124@Github) + +------------------------------------------------------------------------------------------------------------- + +## 4.1.0 + +### 新特性 +* 【extra】 模板工具改为模板门面,抽象各模板引擎 +* 【core】 修改Season为quarter(pr#114@Github) +* 【core】 CollUtil增加removeAny方法 +* 【core】 StrUtil增加emptyToDefault和blankToDefault(issue#115@Github) +* 【core】 优化排列组合算法(感谢@【青岛】LQ) +* 【core】 NumberUtil增加roundHalfEven(感谢@【青岛】LQ) +* 【http】 HttpRequest.form支持多文件上传(相同key)(issue#IJYWM@Gitee) +* 【db】 新增SqlLog,独立SQL日志打印配置 +* 【poi】 ExcelReader新增readAsText方法,ExcelWriter新增setHeaderOrFooter方法(设置页眉页脚) +* 【crypto】 删除DSA类(DSA算法用在Sign中),修改规则,RSA分段方式变为全局(issue#IKGKG@Gitee) +* 【core】 DateUtil添加range和rangeToList方法,增加DateRange类(issue#119@Github) +* 【core】 StrUtil增加concat方法,可选是否null转""(感谢@【帝都】宁静) + +### Bug修复 +* 【core】 修复StrUtil.replace方法第一个字符无法替换问题(issue#IJZR0@Gitee) +* 【core】 修复Season计算问题(pr#114@Github) +* 【core】 修复PinyinUtil获取拼音特殊字符转数字问题(issue#IJNWH@Gitee) +* 【core】 修复FileUtil.isAbsolutePath方法正则问题(issue#IJZUB@Gitee) +* 【extra】 修复ServletUtil.getMultipart方法的问题 +* 【http】 修复patch方法无效问题(issue#IK2Z8@Gitee) +* 【core】 修复DateUtil.parseTimeToday格式问题(issue#IK25B@Gitee) +* 【poi】 修复设置字体日期和小数无效问题(issue#IK488@Gitee) +* 【core】 修复NumberUtil.partValue的bug(pr#15@Gitee) +* 【poi】 调整了readBySax方式读取导致的部分问题 +* 【core】 修复CsvRow的get方法越界问题(issue#IK9CX@Gitee) +* 【core】 修复UnicodeUtil丢失末尾字符串的问题(issue#IKI6T@Gitee) + +------------------------------------------------------------------------------------------------------------- + +## 4.0.13 + +### 新特性 +* 【json】 JSONArray添加jsonIter方法可以实现foreach语法遍历JSONObject(issue#IJPIJ@Gitee) +* 【core】 强化FileTypeUtil中对PDF文件格式的识别兼容性(issue#IJO1K@Gitee) +* 【core】 修改BetweenFormater枚举规则,修复不足1天显示空问题 +* 【http】 由于JDK9移除了javax.activation导致的问题,修复移除相关包依赖(issue#109@Github) +* 【core】 改进Resource,增加getName方法,增加构造支持name +* 【core】 RandomUtil增加randomStringUpper方法(issue#IJVLS@Gitee) + +### Bug修复 +* 【core】 修复XmlUtil.toStr方法注释丢失问题(issue#IJPUA@Gitee) +* 【core】 修复ImageUtil.scale和createFont方法的bug(issue#IJOKE@Gitee) +* 【core】 修复StrUtil.format方法Map参数中值为null导致的空指针问题(issue#IJO31@Gitee) +* 【core】 修复ReUtil.getAllGroups丢失最后一个分组问题(issue#IJRJM@Gitee) +* 【json】 修复Bean中为Map导致的泛型类型不匹配问题(issue#IJRJM@Gitee) + +------------------------------------------------------------------------------------------------------------- + +## 4.0.12 + +### 新特性 +* 【core】 ClassScaner支持jar的嵌套 + +### Bug修复 +* 【setting】 修复Setting中size的bug +* 【cron】 修复Setting修改导致的定时任务读取错误问题(issue#IJMVN@Gitee) +* 【setting】 修复Props中autoLoad无效问题(issue#IJMOE@Gitee) +* 【cron】 修复表达式中年匹配位置的问题(issue#106@Gtihub) +* 【log】 修复log.info(null)空指针问题(issue#IJNRW@Gitee) + +------------------------------------------------------------------------------------------------------------- + +## 4.0.11 + +### 新特性 +* 【core】 Week.toChinese()添加可选参数,选择星期的前缀(比如是“星期”还是“周”) +* 【core】 PinyinUtil增加方法,汉字转拼音(pr#11@Gitee) +* 【core】 Convert增加toList方法 +* 【core】 CollUtil增加toList方法(感谢@【帝都】宁静) +* 【poi】 新增FormulaCellValue对象用于写出公式支持(感谢@【宁波】mojie126) + +### Bug修复 +* 【core】 修复NumberChineseFormater.format()方法无“元”字的问题(issue#IJ6MR@Gitee) +* 【core】 修复FileUtil.loopFile遍历根目录时空指针错误问题 +* 【poi】 修复ExcelReader遇到ERROR单元格时报错问题(感谢@夏夜神话) +* 【http】 修复HttpUtil.post传入json字符串导致的问题(issue#99@Github) +* 【json】 修复Unicode不可见字符转义导致的中文双引号等符号显示问题(issue#IJFBD@Gitee) +* 【core】 修复ReferenceUtil中SoftReference错误问题(pr#105@Github) +* 【db】 删除ActiveRsHandler(歧义),修复showSql属性报错问题(issue#IJII8@Gitee) +* 【setting】 大改Setting逻辑,使用GroupedMap代替分组拼接方式,解决了无分组情况下会包含分组的问题 + +------------------------------------------------------------------------------------------------------------- + +## 4.0.10 + +### 新特性 +* 【poi】 ExcelWriter.merge方法加入重载,可选是否加入默认标题样式 +* 【poi】 ExcelSaxReader改进按照流读取工作簿的构造,使之对于mark不支持的流也可解析 +* 【cron】 添加updatePattern方法,可更新Task执行时间规则(感谢@【上海】嘿) +* 【cache】 添加get方法支持可选的是否更新lastAccess时间(issue#IISC4@Gitee) +* 【core】 StrUtil增加isNullOrUndefined、isEmptyOrUndefined、isBlankOrUndefined方法(issue#IIR44@Gitee) +* 【core】 isBlankChar方法迁移到CharUtil中 +* 【db】 增加NamedSql +* 【poi】 对于POI未引入或版本错误提供更加明确的提示 +* 【core】 增加UUIDConverter,支持UUID对象的自动转换 +* 【core】 IterUtil增加fieldValueList、fieldValueAsMap、join重载方法(issue#IIU4F@Gitee) +* 【core】 IoUtil增加checksum、toBuffered方法,StrUtil增加maxLength方法(参考osgl-tool) +* 【poi】 ExcelReader支持自定义sheet + +### Bug修复 +* 【poi】 修复ExcelWriter合并单元格后样式失效问题 +* 【http】 修复HttpUtil.download方法遇到特殊Disposition时处理异常问题(感谢@【深圳】Bomb) +* 【core】 修复StrUtil.toUnderlineCase方法中下划线转下划线导致的问题 +* 【core】 修复RandomUtil.randomEles方法计数错误问题(issue#98@Github) +* 【core】 修复NumberChineseFormater负数小数结果错误问题(pr#10@Gitee) +* 【captcha】修复验证码无法序列化的问题(issue#IJ2MI@Gitee) + +------------------------------------------------------------------------------------------------------------- + +## 4.0.9 + +### 新特性 +* 【core】 SecureUtil增加signParamsSha1方法(感谢@【帝都】宁静) +* 【core】 XmlUtil增加mapToXml和xmlToMap(感谢@【杭州】小宙子) +* 【captcha】修改逻辑:在创建验证码对象时生成一个验证码(感谢@【重庆】liuuuu) +* 【core】 CopiedIterator使用LinkedList替代ArrayList(issue#III8K@Gitee) +* 【poi】 ExcelWriter增加getOrCreateCell、createStyleForCell方法,便于自定义特殊单元格 +* 【core】 增加AnnotationUtil类 +* 【core】 IoUtil增加toMarkSupportStream方法 +* 【poi】 ExcelReader改进按照流读取工作簿的构造,使之对于mark不支持的流也可解析 +* 【core】 新增BytesResource和InputStreamResource +* 【core】 RandomUtil新增randomBigDecimal(感谢@【帝都】宁静) +* 【db】 Column对象添加comment字段 +* 【core】 Base64增加encode方法,参数为Inputstream和File,新增decodeToFile、decodeToStream(issue#IILZS@Gitee) +* 【core】 扩充XmlUtil部分方法 + +### Bug修复 +* 【core】修复StrUtil.replace问题(感谢@【上海】piaohao) +* 【mail】解决在javax.mail大于1.5版本时,附件名过长在国内邮箱导致的显示错误问题(添加splitlongparameters参数) +* 【core】修复ZipUtil.zip压缩目录时加入盘符问题(感谢@【深圳】Vmo ) +* 【core】修复PropertyComparator失效问题(感谢@【长沙】哼哼 ) +* 【cron】修复20/2此类表达式无效问题(感谢@【广州】杨小过 ) +* 【core】修复XmlUtil.toStr编码设置无效问题 + +------------------------------------------------------------------------------------------------------------- + +## 4.0.8 + +### 新特性 +* 【core】新增PinyinComparator、CollUtil新增sortByPinyin(感谢@【帝都】宁静) +* 【json】JSONUtil增加xmlToJson方法 +* 【poi】 ExcelWriter增加setColumnWidth和setRowHeight方法 +* 【core】FileUtil.clean增加字符串重载(感谢@【帝都】宁静) +* 【core】ArrayUtil增加insert方法(感谢@【帝都】宁静) +* 【core】RandomUtil.randomDouble增加可选保留小数重载(感谢@【帝都】宁静) +* 【core】增加RandomUtil.randomDay随机天(感谢@【帝都】宁静) +* 【poi】 ExcelWriter增加setOrCreateSheet方法,从而支持多sheet生成 + +### Bug修复 +* 【json】修复JSONArray中addAll加入两次的bug(感谢@【天津】〓下页) +* 【core】修复BeanDesc中对static属性未忽略的问题(感谢@【深圳】枫林晓寒) +* 【http】解决无法移除默认头信息的问题 +* 【core】修复Base64在decode时针对urlSafe乱码问题(issue#89@Github) +* 【core】修复ReUtil.extractMulti(感谢@【杭州】徐承恩) +* 【core】修复DESede类中算法错误问题(issue#93@Github) + +------------------------------------------------------------------------------------------------------------- + +## 4.0.7 + +### 新特性 +* 【core】新加math包,并添加MathUtil工具类(排列组合迁入此) +* 【core】StrUtil增加move方法,字符串位移(感谢@【帝都】宁静) +* 【core】ArrayUtil的max和min采用可变参数(T[]除外)(感谢@【帝都】宁静) +* 【core】NumberUtil增加max和min方法,与ArrayUtil一致(感谢@【帝都】宁静) +* 【poi】 去除InternalExcelUtil,根据功能新增WorkbookUtil、RowUtil、CellUtil、ExcelPicUtil +* 【core】新增PinyinUtil(感谢@【帝都】宁静) +* 【core】StrUtil增加wrapAll、wrapAllIfMissing(感谢@【帝都】宁静) +* 【core】Singleton增加put方法 +* 【core】Convert增加convertByClassName方法 +* 【json】JSONUtil增加toList快捷方法 + +### Bug修复 +* 【core】修复排列组合结果错误问题(感谢@【帝都】宁静) +* 【poi】 修复StrUtil.unWrap传入null导致的越界问题(issue#II1VU@Gitee) +* 【core】修复ImageUtil.sliceByRowsAndCols方法计算错误(感谢@【唐山】小虫) +* 【core】修复StrUtil.replace问题(感谢@【霾都】QQ小冰) +* 【core】修复FileTypeUtil对jpg的识别范围(issue#91@Github) + +------------------------------------------------------------------------------------------------------------- + +## 4.0.6 + +### 新特性 +* 【poi】 ExcelReader增加getWriter、getOrCreateCell方法 +* 【core】NetUtil增加isInRange方法(感谢@【成都】小邓) +* 【core】新增BeanPath(仅支持部分JSONPath语法) +* 【core】CollUtil新增reverse、reverseNew方法 +* 【core】集合中新增排列(Arrangement)和组合(Combination)类(感谢@【北京】宁静) +* 【core】StrUtil新增splitToLong和splitToInt方法 +* 【core】MapUtil增加getXXX方法 +* 【core】扩充Dict构造 +* 【core】CollUtil新增sortByProperty方法 +* 【json】toBean支持下划线转驼峰 +* 【core】FileUtil新增更多方法,包括路径拼接 +* 【core】新增LineIterator、NullOutputStream两个类 + +### Bug修复 +* 【core】修复IdcardUtil中身份证15转18位年的问题(Issue#IHT1Q@Gitee) +* 【http】忽略Premature EOF错误(感谢@【南京】peckey) +* 【core】修复ArrayConvert中集合转原始类型数组导致的异常 + +------------------------------------------------------------------------------------------------------------- + +## 4.0.5 + +### 新特性 +* 【json】 toBean方法支持Map.class参数,消除歧义 +* 【core】FileWriter和FileUtil增加writeMap方法 +* 【core】新增CsvWriter和CsvUtil +* 【poi】 改进ExcelWriter.flush未指定文件时的报错信息 +* 【db】 在配置文件不存在时优化错误提示 +* 【core】BeanUtil.beanToMap方法支持自定义key +* 【core】增加ModifierUtil,修饰符工具类 +* 【http】下载文件时文件名首先从头信息中获取 +* 【poi】 ExcelReader增加getCell方法 +* 【db】 Oracle驱动变更 +* 【extra】扩充Sftp方法(感谢@【广西】Succy) +* 【core】ImageUtil增加binary方法,生成二值化图片(感谢@【天津】〓下页) + +### Bug修复 +* 【poi】 修复ExcelReader获取Workbook为空的问题 +* 【core】修复ImageUtil.scale的问题(感谢@【北京】千古不见一人闲) +* 【json】 修复JSON转字符串时值中双引号转义问题(感谢@【深圳】jae) + +------------------------------------------------------------------------------------------------------------- + +## 4.0.4 + +### 新特性 +* 【http】 HttpUtil.downloadFile增加超时重载(感谢@【深圳】富) +* 【setting】Setting增加构造重载(pr#8@Gitee) +* 【core】 IterUtil增加fieldValueMap方法(感谢@【苏州】陈华 万缕数据@【北京】宁静) + +### Bug修复 +* 【log】 修复StaticLog.warn打印级别错误问题(issue#IHMF9@Gitee) +* 【core】修复MapUtil.newHashMap中isOrder(感谢@【珠海】hzhhui) +* 【core】修复DateTime.season获取的问题(感谢@西湖断桥) +* 【cron】修复在秒匹配关闭时无法匹配的问题(感谢@【北京】宁静) + +------------------------------------------------------------------------------------------------------------- + +## 4.0.3 + +### 新特性 +* 【core】新增LocalPortGenerater,本地端口生成器 +* 【extra】新增Sftp类,用于SFTP支持 +* 【core】StrUtil增加replace(支持参数从某个位置开始)和replaceIgnoreCase方法(感谢@【贵阳】shadow ) +* 【core】Number.equals方法迁移到CharUtil(NumberUtil中依旧保留) +* 【extra】mail增加抄送和密送支持(感谢【成都】出错) +* 【poi】ExcelReader别名在返回List时也被支持(第一行) +* 【poi】ExcelReader增加getSheets和getSheetNames方法(感谢@【帝都】宁静) +* 【poi】ExcelReader增加readCellValue和readRow方法(感谢@【苏州】马克) +* 【db】全局数据源工厂独立,使用懒加载方式,消除歧义 +* 【log】全局日志工厂独立,懒加载方式,消除歧义 +* 【extra】MailUtil增加快捷方法支持抄送和密送参数 + +### Bug修复 +* 【core】修复获取子路径bug(issue#IHI5K@Gitee) +* 【poi】修复ExcelReader在读取文件后未关闭导致文件被占用问题(感谢@【昆明】-@_@) +* 【log】解决Tinylog实现显示类名和行行错误问题 +* 【extra】修复Mail构造在MailAccount传入null时读取错误的问题 + +------------------------------------------------------------------------------------------------------------- + +## 4.0.2 + +### 新特性 +* 【core】优化BeanDesc,适配更多Getter和Setter方法 +* 【extra】增加基于zxing的二维码生成和解码(zxing可选依赖) +* 【core】增加VersionComparator用于版本比较,同时添加StrUtil.compareVersion +* 【core】Convert支持Map、Bean之间的转换、enum,新增BeanConverter和CastBeanConverter +* 【extra】ServletUtil中增加获取body和上传文件支持 +* 【json】在json与bean互相转换时支持enum和字符串转换(感谢@【帝都】宁静) +* 【core】增加OptArrayTypeGetter接口 +* 【http】HttpUtil增加decodeParamMap方法,返回单值map(感谢@【帝都】宁静) +* 【poi】ExcelWriter增加writeCellValue方法 +* 【cron】去除CronUtil以及Scheduler中的isMatchYear方法(年的匹配通过表达式自动判断) +* 【extra】邮件Mail对象增加setUseGlobalSession方法,用于自定义是否使用单例会话 + +### Bug修复 +* 【setting】修复clear方法未清空group的问题,store方法未换行问题,set方法分组丢失问题(感谢@【广西】Succy) +* 【json】修复Map嵌套转JSONObject时判断失误导致的值错误(issue#@Gitee) +* 【core】修复betweenYear注释错误(感谢@【常州】在校学生) +* 【core】修复Convert.digitToChinese方法中角为0时显示问题(issue#IHHE1@Gitee) +* 【cron】修复在秒匹配模式下5位表达式执行异常问题,修复cron.setting文件不存在报错问题 +* 【extra】邮件配置中参数值转为String解决可能存在的bug + +------------------------------------------------------------------------------------------------------------- + +## 4.0.1 + +### 新特性 +* 新增CharUtil +* 新增ASCIIStrCache,对ASCII字符做String对应表,提升字符转字符串性能 +* 去除JschUtil中的同步修饰,改为锁 +* 新增MapUtil.sort +* SymmetricCrypto支持加密后转为Base64和从Base64解密 +* AsymmetricCrypto支持Hex和Base64加密解密 +* 新增SecureUtil.signParams方法用于参数签名(感谢@【帝都】宁静) +* 新增Loader和LazyLoader,抽象懒加载 +* 新增CsvReader,CSV读取 +* HttpRequest支持可选get请求下的url参数编码 +* ExcelReader增加read重载方法,ExcelUtil增加isEmpty(Sheet)方法(pr#5@Gitee) +* db模块针对IS NULL优化 + +### Bug修复 +* 修复db模块中数据库为下划线而Bean为驼峰导致的注入失败问题(感谢@【广西】Succy) +* 修复findLike的bug(感谢@cici) +* 修复ArrayUtil.join循环引用bug +* FileTypeUtil针对pdf格式做修改(issue#IHDNH@Gitee) +* 修复Http模块中get方法拼接参数问题 +* 修复db模块in方式查询错误问题 +* 修复CollUtil.disjunction计算差集修复一个集合为空的情况(感谢@【天津】〓下页) +* 修复Db模块中Number参数丢失问题(感谢@【山东】小灰灰) + +------------------------------------------------------------------------------------------------------------- + +## 4.0.0 + +### 新特性 +* 变更包名为cn.hutool.xxx +* 新增ObjecIdt类,用于实现MongoDB的ID生成策略 +* 验证码单独成为一个模块hutool-captcha +* 新增NamedThreadFactory +* 新增BufferUtil +* POI新增StyleUtil,StyleSet新增方法可设置背景、边框等样式 +* JDBC参数针对BigInteger处理 +* db模块支持显示和格式化显示SQL +* 调整日志优先级:ConsoleLog优先于JDKLog,Log4j2优先于Log4j +* db模块的SqlRunner中可自定义Wrapper +* ExcelReader增加read重载方法(pr#4@Gitee) +* Convert.convert增加Class的重载,解决返回值歧义(感谢@t-io) +* Http中使用byte[]存储body,减少转换 +* ExcelReader增加getWorkbook、getSheet方法 +* 新增StrBuilder +* 新增JschUtil +* 新增UnicodeUtil +* db模块的BeanListHandler和BeanHandler支持Map、Collection、Array等类型 +* NumberUtil加减乘支持多个值,解决float和double混合运算导致的坑 + +### Bug修复 +* 修复ExcelReader空行导致空指针问题(pr#4@Gitee) +* 修复BeanUtil.getProperty不能获取父类属性的问题 +* 修复BeanDesc类中boolean类型字段名为isXXX的情况无法注入问题 +* 解决类扫描后加载类中引用依赖导致的报错(感谢@【帝都】宁静) diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 000000000..c7937dbd2 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,121 @@ + 木兰宽松许可证, 第1版 + + 木兰宽松许可证, 第1版 + 2019年8月 http://license.coscl.org.cn/MulanPSL + + 您对“软件”的复制、使用、修改及分发受木兰宽松许可证,第1版(“本许可证”)的如下条款的约束: + + 0. 定义 + + “软件”是指由“贡献”构成的许可在“本许可证”下的程序和相关文档的集合。 + + “贡献者”是指将受版权法保护的作品许可在“本许可证”下的自然人或“法人实体”。 + + “法人实体”是指提交贡献的机构及其“关联实体”。 + + “关联实体”是指,对“本许可证”下的一方而言,控制、受控制或与其共同受控制的机构,此处的控制是指有受控方或共同受控方至少50%直接或间接的投票权、资金或其他有价证券。 + + “贡献”是指由任一“贡献者”许可在“本许可证”下的受版权法保护的作品。 + + 1. 授予版权许可 + + 每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的版权许可,您可以复制、使用、修改、分发其“贡献”,不论修改与否。 + + 2. 授予专利许可 + + 每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的(根据本条规定撤销除外)专利许可,供您制造、委托制造、使用、许诺销售、销售、进口其“贡献”或以其他方式转移其“贡献”。前述专利许可仅限于“贡献者”现在或将来拥有或控制的其“贡献”本身或其“贡献”与许可“贡献”时的“软件”结合而将必然会侵犯的专利权利要求,不包括仅因您或他人修改“贡献”或其他结合而将必然会侵犯到的专利权利要求。如您或您的“关联实体”直接或间接地(包括通过代理、专利被许可人或受让人),就“软件”或其中的“贡献”对任何人发起专利侵权诉讼(包括反诉或交叉诉讼)或其他专利维权行动,指控其侵犯专利权,则“本许可证”授予您对“软件”的专利许可自您提起诉讼或发起维权行动之日终止。 + + 3. 无商标许可 + + “本许可证”不提供对“贡献者”的商品名称、商标、服务标志或产品名称的商标许可,但您为满足第4条规定的声明义务而必须使用除外。 + + 4. 分发限制 + + 您可以在任何媒介中将“软件”以源程序形式或可执行形式重新分发,不论修改与否,但您必须向接收者提供“本许可证”的副本,并保留“软件”中的版权、商标、专利及免责声明。 + + 5. 免责声明与责任限制 + + “软件”及其中的“贡献”在提供时不带任何明示或默示的担保。在任何情况下,“贡献者”或版权所有者不对任何人因使用“软件”或其中的“贡献”而引发的任何直接或间接损失承担责任,不论因何种原因导致或者基于何种法律理论,即使其曾被建议有此种损失的可能性。 + + 条款结束。 + + 如何将木兰宽松许可证,第1版,应用到您的软件 + + 如果您希望将木兰宽松许可证,第1版,应用到您的新软件,为了方便接收者查阅,建议您完成如下三步: + + 1, 请您补充如下声明中的空白,包括软件名、软件的首次发表年份以及您作为版权人的名字; + + 2, 请您在软件包的一级目录下创建以“LICENSE”为名的文件,将整个许可证文本放入该文件中; + + 3, 请将如下声明文本放入每个源文件的头部注释中。 + + Copyright (c) [2019] [name of copyright holder] + [Software Name] is licensed under the Mulan PSL v1. + You can use this software according to the terms and conditions of the Mulan PSL v1. + You may obtain a copy of Mulan PSL v1 at: + http://license.coscl.org.cn/MulanPSL + THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR + PURPOSE. + See the Mulan PSL v1 for more details. + + + Mulan Permissive Software License,Version 1 + + Mulan Permissive Software License,Version 1 (Mulan PSL v1) + August 2019 http://license.coscl.org.cn/MulanPSL + + Your reproduction, use, modification and distribution of the Software shall be subject to Mulan PSL v1 (this License) with following terms and conditions: + + 0. Definition + + Software means the program and related documents which are comprised of those Contribution and licensed under this License. + + Contributor means the Individual or Legal Entity who licenses its copyrightable work under this License. + + Legal Entity means the entity making a Contribution and all its Affiliates. + + Affiliates means entities that control, or are controlled by, or are under common control with a party to this License, ‘control’ means direct or indirect ownership of at least fifty percent (50%) of the voting power, capital or other securities of controlled or commonly controlled entity. + + Contribution means the copyrightable work licensed by a particular Contributor under this License. + + 1. Grant of Copyright License + + Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable copyright license to reproduce, use, modify, or distribute its Contribution, with modification or not. + + 2. Grant of Patent License + + Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable (except for revocation under this Section) patent license to make, have made, use, offer for sale, sell, import or otherwise transfer its Contribution where such patent license is only limited to the patent claims owned or controlled by such Contributor now or in future which will be necessarily infringed by its Contribution alone, or by combination of the Contribution with the Software to which the Contribution was contributed, excluding of any patent claims solely be infringed by your or others’ modification or other combinations. If you or your Affiliates directly or indirectly (including through an agent, patent licensee or assignee), institute patent litigation (including a cross claim or counterclaim in a litigation) or other patent enforcement activities against any individual or entity by alleging that the Software or any Contribution in it infringes patents, then any patent license granted to you under this License for the Software shall terminate as of the date such litigation or activity is filed or taken. + + 3. No Trademark License + + No trademark license is granted to use the trade names, trademarks, service marks, or product names of Contributor, except as required to fulfill notice requirements in section 4. + + 4. Distribution Restriction + + You may distribute the Software in any medium with or without modification, whether in source or executable forms, provided that you provide recipients with a copy of this License and retain copyright, patent, trademark and disclaimer statements in the Software. + + 5. Disclaimer of Warranty and Limitation of Liability + + The Software and Contribution in it are provided without warranties of any kind, either express or implied. In no event shall any Contributor or copyright holder be liable to you for any damages, including, but not limited to any direct, or indirect, special or consequential damages arising from your use or inability to use the Software or the Contribution in it, no matter how it’s caused or based on which legal theory, even if advised of the possibility of such damages. + + End of the Terms and Conditions + + How to apply the Mulan Permissive Software License,Version 1 (Mulan PSL v1) to your software + + To apply the Mulan PSL v1 to your work, for easy identification by recipients, you are suggested to complete following three steps: + + i. Fill in the blanks in following statement, including insert your software name, the year of the first publication of your software, and your name identified as the copyright owner; + ii. Create a file named “LICENSE” which contains the whole context of this License in the first directory of your software package; + iii. Attach the statement to the appropriate annotated syntax at the beginning of each source file. + + Copyright (c) [2019] [name of copyright holder] + [Software Name] is licensed under the Mulan PSL v1. + You can use this software according to the terms and conditions of the Mulan PSL v1. + You may obtain a copy of Mulan PSL v1 at: + http://license.coscl.org.cn/MulanPSL + THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR + PURPOSE. + + See the Mulan PSL v1 for more details. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000..8ce7782c0 --- /dev/null +++ b/README.md @@ -0,0 +1,175 @@ +

+ +

+

+ A set of tools that keep Java sweet. +

+

+ + + + + + + + + + + + + + + + + + + + + + gitee star + + + github star + + + netlify + +

+

+ -- 主页:https://hutool.cn/ | https://www.hutool.club/ -- +

+

+ -- QQ群③:555368316 -- + -- QQ群④:718802356 -- +

+ +------------------------------------------------------------------------------- + +## 简介 + +Hutool是一个Java工具包,也只是一个工具包,它帮助我们简化每一行代码,减少每一个方法,让Java语言也可以“甜甜的”。它最初是作者项目中“util”包的一个整理,后来慢慢积累并加入更多非业务相关功能,并广泛学习其它开源项目精髓,经过自己整理修改,最终形成丰富的开源工具集。 + +Hutool是Hu + tool的自造词,谐音“糊涂”,寓意,追求“万事都作糊涂观,无所谓失,无所谓得”的境界。 + +### Hutool如何改变我们的coding方式 + +Hutool的目标是使用一个工具方法代替一段复杂代码,从而最大限度的避免“复制粘贴”代码的问题,彻底改变我们写代码的方式。 + +以计算MD5为例: + +- 【以前】打开百度 -> 搜“Java MD5加密” -> 打开某篇博客-> 复制粘贴 -> 改改好用 +- 【现在】引入Hutool -> SecureUtil.md5() + +同样,当我们想实现什么功能,脑袋中第一个想到的就是去找XXXUtil,而非百度。 + +------------------------------------------------------------------------------- + +## 包含组件 +一个Java基础工具类,对文件、流、加密解密、转码、正则、线程、XML等JDK方法进行封装,组成各种Util工具类,同时提供以下组件: + +- hutool-aop JDK动态代理封装,提供非IOC下的切面支持 +- hutool-bloomFilter 布隆过滤,提供一些Hash算法的布隆过滤 +- hutool-cache 简单缓存实现 +- hutool-core 核心,包括Bean操作、日期、各种Util等 +- hutool-cron 定时任务模块,提供类Crontab表达式的定时任务 +- hutool-crypto 加密解密模块,提供对称、非对称和摘要算法封装 +- hutool-db JDBC封装后的数据操作,基于ActiveRecord思想 +- hutool-dfa 基于DFA模型的多关键字查找 +- hutool-extra 扩展模块,对第三方封装(模板引擎、邮件、Servlet、二维码、Emoji、FTP、分词等) +- hutool-http 基于HttpUrlConnection的Http客户端封装 +- hutool-log 自动识别日志实现的日志门面 +- hutool-script 脚本执行封装,例如Javascript +- hutool-setting 功能更强大的Setting配置文件和Properties封装 +- hutool-system 系统参数调用封装(JVM信息等) +- hutool-json JSON实现 +- hutool-captcha 图片验证码实现 +- hutool-poi 针对POI中Excel的封装 +- hutool-socket 基于Java的NIO和AIO的Socket封装 + +可以根据需求对每个模块单独引入,也可以通过引入`hutool-all`方式引入所有模块。 + +------------------------------------------------------------------------------- + +## 文档 + +[中文文档](https://www.hutool.cn/docs/) +[中文文档(备用)](https://www.hutool.club/docs/) + +[参考API](https://apidoc.gitee.com/loolly/hutool/) + +------------------------------------------------------------------------------- + +## 安装 + +### Maven +在项目的pom.xml的dependencies中加入以下内容: + +```xml + + cn.hutool + hutool-all + 4.6.2 + +``` + +### Gradle +``` +compile 'cn.hutool:hutool-all:4.6.2' +``` + +### 非Maven项目 + +点击以下任一链接,下载`hutool-all-X.X.X.jar`即可: + +- [Maven中央库1](https://repo1.maven.org/maven2/cn/hutool/hutool-all/4.6.2/) +- [Maven中央库2](http://repo2.maven.org/maven2/cn/hutool/hutool-all/4.6.2/) + +> 注意 +> Hutool支持JDK7+,对Android平台没有测试,不能保证所有工具类获工具方法可用。 + +### 编译安装 + +访问Hutool的码云主页:[https://gitee.com/loolly/hutool](https://gitee.com/loolly/hutool) 下载整个项目源码(v4-master或v4-dev分支都可)然后进入Hutool项目目录执行: + +```sh +./hutool.sh install +``` + +然后就可以使用Maven引入了。 + +------------------------------------------------------------------------------- + +## 添砖加瓦 + +### 提供bug反馈或建议 + +- [码云Gitee](https://gitee.com/loolly/hutool/issues) +- [Github](https://github.com/looly/hutool/issues) + +### 遵照的原则 + +Hutool欢迎任何人为Hutool添砖加瓦,贡献代码,不过作者是一个强迫症患者,为了照顾病人,需要提交的pr(pull request)符合一些规范,规范如下: + +1. 注释完备,尤其每个新增的方法应按照Java文档规范标明方法说明、参数说明、返回值说明等信息,如果愿意,也可以加上你的大名。 +2. Hutool的缩进按照Eclipse(不要跟我说IDEA多好用,作者非常懒,学不会)默认(tab)缩进,所以请遵守(不要和我争执空格与tab的问题,这是一个病人的习惯)。 +3. 新加的方法不要使用第三方库的方法,Hutool遵循无依赖原则(除非在extra模块中加方法工具)。 +4. 请pull request到`v4-dev`分支。Hutool在4.x版本后使用了新的分支:`v4-master`是主分支,表示已经发布中央库的版本,这个分支不允许pr,也不允许修改。`v4-dev`分支是开发分支,Hutool的下个版本或者SNAPSHOT版本在这个分支上开发,你可以pr到这个分支。 + +### 贡献代码的步骤 + +1. 在Gitee或者Github上fork项目到自己的repo +2. 把fork过去的项目也就是你的项目clone到你的本地 +3. 修改代码(记得一定要修改v4-dev分支) +4. commit后push到自己的库(v4-dev分支) +5. 登录Gitee或Github在你首页可以看到一个 pull request 按钮,点击它,填写一些说明信息,然后提交即可。 +6. 等待作者合并 + +------------------------------------------------------------------------------- + +## 捐赠 + +如果你觉得Hutool不错,可以捐赠请作者吃包辣条~,在此表示感谢^_^。 + +点击以下链接,将页面拉到最下方点击“捐赠”即可。 + +[前往捐赠](https://gitee.com/loolly/hutool) \ No newline at end of file diff --git a/bin/check_dependency_updates.sh b/bin/check_dependency_updates.sh new file mode 100755 index 000000000..2003a87b0 --- /dev/null +++ b/bin/check_dependency_updates.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +#-------------------------------------- +# Check dependency, thanks to t-io +#-------------------------------------- + +mvn versions:display-dependency-updates diff --git a/bin/commit.sh b/bin/commit.sh new file mode 100755 index 000000000..bd9b0b68b --- /dev/null +++ b/bin/commit.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +git add . +git commit -am "$1" + +bin/push_dev.sh diff --git a/bin/deploy.sh b/bin/deploy.sh new file mode 100755 index 000000000..f49112519 --- /dev/null +++ b/bin/deploy.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +mvn clean deploy -P release diff --git a/bin/install.sh b/bin/install.sh new file mode 100755 index 000000000..cc34f01db --- /dev/null +++ b/bin/install.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +exec mvn clean source:jar javadoc:javadoc install -Dmaven.test.skip=false -Dmaven.javadoc.skip=false diff --git a/bin/javadoc.sh b/bin/javadoc.sh new file mode 100755 index 000000000..b9aaa3914 --- /dev/null +++ b/bin/javadoc.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +exec mvn javadoc:javadoc diff --git a/bin/logo.sh b/bin/logo.sh new file mode 100755 index 000000000..36a14646a --- /dev/null +++ b/bin/logo.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +echo '========================================' +echo ' __ __ __ __ ' +echo ' / / / /__ __ / /_ ____ ____ / / ' +echo ' / /_/ // / / // __// __ \ / __ \ / / ' +echo ' / __ // /_/ // /_ / /_/ // /_/ // / ' +echo '/_/ /_/ \____/ \__/ \____/ \____//_/ ' +echo '' +echo '-----------http://hutool.cn/------------' +echo '========================================' diff --git a/bin/push_dev.sh b/bin/push_dev.sh new file mode 100755 index 000000000..e46f09703 --- /dev/null +++ b/bin/push_dev.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +echo -e "\033[32mCheckout to v4-dev\033[0m" +git checkout v4-dev + +echo -e "\033[32mPush to origin v4-dev\033[0m" +git push origin v4-dev +echo -e "\033[32mPush to osc v4-dev\033[0m" +git push osc v4-dev diff --git a/bin/push_master.sh b/bin/push_master.sh new file mode 100755 index 000000000..34d12496c --- /dev/null +++ b/bin/push_master.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +echo -e "\033[32mCheckout to v4-master\033[0m" +git checkout v4-master + +echo -e "\033[32mMerge v4-dev branch\033[0m" +git merge v4-dev -m 'Prepare release' + +echo -e "\033[32mPush to origin v4-master\033[0m" +git push origin v4-master +echo -e "\033[32mPush to osc v4-master\033[0m" +git push osc v4-master diff --git a/bin/replaceVersion.sh b/bin/replaceVersion.sh new file mode 100755 index 000000000..1dab3e58e --- /dev/null +++ b/bin/replaceVersion.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +#----------------------------------------------------------- +# 此脚本用于每次升级Hutool时替换相应位置的版本号 +#----------------------------------------------------------- + +set -o errexit + +pwd=$(pwd) + +echo "当前路径:${pwd}" + +if [ -n "$1" ];then + new_version="$1" + old_version=`cat ${pwd}/bin/version.txt` + echo "$old_version 替换为新版本 $new_version" +else + # 参数错误,退出 + echo "ERROR: 请指定新版本!" + exit +fi + +if [ ! -n "$old_version" ]; then + echo "ERROR: 旧版本不存在,请确认bin/version.txt中信息正确" + exit +fi + +# 替换README.md中的版本 +sed -i "s/${old_version}/${new_version}/g" $pwd/README.md +# 替换docs/index.html中的版本 +sed -i "s/${old_version}/${new_version}/g" $pwd/docs/js/version.js + +# 保留新版本号 +echo "$new_version" > $pwd/bin/version.txt diff --git a/bin/test.sh b/bin/test.sh new file mode 100755 index 000000000..a91e811a6 --- /dev/null +++ b/bin/test.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +exec mvn test diff --git a/bin/update_version.sh b/bin/update_version.sh new file mode 100755 index 000000000..763f95b0a --- /dev/null +++ b/bin/update_version.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +#------------------------------------------------ +# 升级Hutool版本,包括: +# 1. 升级pom.xml中的版本号 +# 2. 替换README.md和docs中的版本号 +#------------------------------------------------ + +if [ ! -n "$1" ]; then + echo "ERROR: 新版本不存在,请指定参数1" + exit +fi + +# 替换所有模块pom.xml中的版本 +mvn versions:set -DnewVersion=$1 + +# 不带-SNAPSHOT的版本号,用于替换其它地方 +version=${1%-SNAPSHOT} + +# 替换其它地方的版本 +$(pwd)/bin/replaceVersion.sh "$version" diff --git a/bin/version.txt b/bin/version.txt new file mode 100755 index 000000000..c78c4964c --- /dev/null +++ b/bin/version.txt @@ -0,0 +1 @@ +4.6.2 diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 000000000..e69de29bb diff --git a/docs/docs/index.html b/docs/docs/index.html new file mode 100644 index 000000000..b3b7343bc --- /dev/null +++ b/docs/docs/index.html @@ -0,0 +1,74 @@ + + + + + Hutool参考文档 + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 000000000..9279ef032 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,497 @@ + + + + + + + + + + + + + + + + + + + + + + + Hutool — A set of tools that keep Java sweet. + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+
+
+
+
+

+ Hutool + v{{version}} +

+

A set of tools that keep Java sweet.

+ +
+
+
+
+
+ + +
+ +
+ +
+
+ +
+ +
+ +
+

Hutool是Hu + tool的自造词,前者致敬我的“前任公司”,后者为工具之意,谐音“糊涂”,寓意追求“万事都作糊涂观,无所谓失,无所谓得”的境界。

+

Hutool是一个Java工具包,也只是一个工具包,它帮助我们简化每一行代码,减少每一个方法,让Java语言也可以“甜甜的”。Hutool最初是我项目中“util”包的一个整理,后来慢慢积累并加入更多非业务相关功能,并广泛学习其它开源项目精髓,经过自己整理修改,最终形成丰富的开源工具集。

+
    +
  • Web开发
  • +
  • 与其它框架无耦合
  • +
  • 高度可替换
  • +
+
+ +
+

Hutool的设计思想是尽量减少重复的定义,让项目中的util这个package尽量少,总的来说有如下的几个思想:

+
    +
  • 方法优先于对象
  • +
  • 自动识别优于用户定义
  • +
  • 便捷性与灵活性并存
  • +
  • 适配与兼容
  • +
  • 可选依赖原则
  • +
  • 无侵入原则
  • +
+
+ +
+
Maven:在项目的pom.xml的dependencies中加入以下内容:
+
+									<dependency>
+									      <groupId>cn.hutool</groupId>
+									      <artifactId>hutool-all</artifactId>
+									      <version>{{version}}</version>
+									  </dependency>
+								
+
Gradle:
+
+									compile 'cn.hutool:hutool-all:{{version}}'
+								
+

+ 从Maven安装 +

+
+ +
+ +
+ +
+
+ +
+
+ +
+
+ + + + Watch Video +
+
+
+

Hutool 是什么

+

Hutool是一个Java工具包类库,对文件、流、加密解密、转码、正则、线程、XML等JDK方法进行封装,组成各种Util工具类

+
+
+
+
+

日期工具

+

通过DateUtil类,提供高度便捷的日期访问、处理和转换方式。

+
+
+
+
+
+

HTTP客户端

+

通过HttpUtil对HTTP客户端的封装,实现便捷的HTTP请求,并简化文件上传操作。

+
+
+
+
+
+

转换工具

+

通过Convert类中的相应静态方法,提供一整套的类型转换解决方案,并通过ConverterRegistry工厂类自定义转换。

+
+
+ +
+
+
+

配置文件工具(Setting)

+

通过Setting对象,提供兼容Properties文件的更加强大的配置文件工具,用于解决中文、分组等JDK配置文件存在的诸多问题。

+
+
+
+
+
+

日志工具

+

Hutool的日志功能,通过抽象Log接口,提供对Slf4j、LogBack、Log4j、JDK-Logging的全面兼容支持。

+
+
+
+
+
+

JDBC工具类(DB模块)

+

通过db模块,提供对MySQL、Oracle等关系型数据库的JDBC封装,借助ActiveRecord思想,大大简化数据库操作。

+
+
+
+

Hutool的更多功能,期待你的探索:

+

+ 参考文档 + API 文档 +

+
+ +
+ +
+ + +
+
+
+

开发团队

+

我们不是一个人在战斗

+
+
+
+ +

路小磊

+

二手Java码农,Python和前端爱好者

+

一个非职业的码农,混迹于非IT圈子,利用8小时之外做自己喜欢的事情,爱前端,爱数码,爱美女。

+ +
+ +
+
+
+ +

深山码农

+

崇拜自由的生活和善良的人性

+

深山耕耘互金行业多年,熟悉互金系统架构和设计,喜欢研究新技术,善于发现和解决问题

+ +
+
+
+
+ +

Chinaboy

+

相信自己,明天会更好

+

一个奔波于IT圈子的程序猿,拥有自己的梦想,喜欢美女、喜欢音乐、爱打篮球儿...

+ +
+
+
+
+ +

汪汪90

+

悲观的乐观主义者

+

Java程序员一枚,喜欢从生活中领悟技术,喜欢关注技术细节,ennio morricone 音乐的死忠粉。

+ +
+
+
+
+ +

普辉辉

+

java码农,爱技术、爱旅游

+

java码农,爱技术、爱旅游、一直活跃在互联网技术圈。
 

+ +
+
+
+
+ +

普向东

+

需求分析,项目沟通,提供解决方案

+

喜欢编程类工作,喜欢使用代码解决问题的成功感,信仰代码力量,码出高效。
 

+
+
+
+
+ + +
+
+
+

加入讨论

+

通过以下方式加入讨论,或为Hutool添砖加瓦

+
+
+
+ + 555368316 + | 718802356 +
+ +
+ + Gitee Issues +
+ +
+ +
+
+ + +
+
+
+

赞助商

+

为Hutool提供赞助,也许他们也会为你提供更优惠的服务

+
+
+
+
+
+ + +
+
+
+

友情链接

+

为Hutool提供各种帮助和支持的朋友们,我们一起共奋进

+
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + +
+
+
+
+

+ © 2019 Hutool Project. All Rights Reserved.
+ Designed by Looly, Hosted by Coding Pages. +

+
+
+
    +
  • +
  • +
  • +
+
+
+
+
+
+ +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/js/version.js b/docs/js/version.js new file mode 100644 index 000000000..c46ddd3b1 --- /dev/null +++ b/docs/js/version.js @@ -0,0 +1 @@ +var version = '4.6.2' \ No newline at end of file diff --git a/hutool-all/pom.xml b/hutool-all/pom.xml new file mode 100644 index 000000000..767bdb681 --- /dev/null +++ b/hutool-all/pom.xml @@ -0,0 +1,140 @@ + + + 4.0.0 + + jar + + + cn.hutool + hutool-parent + 4.6.2-SNAPSHOT + + + hutool-all + ${project.artifactId} + 提供丰富的Java工具方法,此模块为Hutool所有模块的打包汇总,最终形式为一个jar包 + https://github.com/looly/hutool + + + + cn.hutool + hutool-core + ${project.parent.version} + + + cn.hutool + hutool-aop + ${project.parent.version} + + + cn.hutool + hutool-bloomFilter + ${project.parent.version} + + + cn.hutool + hutool-cache + ${project.parent.version} + + + cn.hutool + hutool-crypto + ${project.parent.version} + + + cn.hutool + hutool-db + ${project.parent.version} + + + cn.hutool + hutool-dfa + ${project.parent.version} + + + cn.hutool + hutool-extra + ${project.parent.version} + + + cn.hutool + hutool-http + ${project.parent.version} + + + cn.hutool + hutool-log + ${project.parent.version} + + + cn.hutool + hutool-script + ${project.parent.version} + + + cn.hutool + hutool-setting + ${project.parent.version} + + + cn.hutool + hutool-system + ${project.parent.version} + + + cn.hutool + hutool-cron + ${project.parent.version} + + + cn.hutool + hutool-json + ${project.parent.version} + + + cn.hutool + hutool-poi + ${project.parent.version} + + + cn.hutool + hutool-captcha + ${project.parent.version} + + + cn.hutool + hutool-socket + ${project.parent.version} + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.1 + + + package + + shade + + + + true + + + ${project.groupId}:*:* + + + + + + + + + + diff --git a/hutool-all/src/main/java/cn/hutool/Hutool.java b/hutool-all/src/main/java/cn/hutool/Hutool.java new file mode 100644 index 000000000..94cd56878 --- /dev/null +++ b/hutool-all/src/main/java/cn/hutool/Hutool.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2017 hutool.cn + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool; + +/** + *

+ * Hutool是Hu + tool的自造词,前者致敬我的“前任公司”,后者为工具之意,谐音“糊涂”,寓意追求“万事都作糊涂观,无所谓失,无所谓得”的境界。 + *

+ * + *

+ * Hutool是一个Java工具包,也只是一个工具包,它帮助我们简化每一行代码,减少每一个方法,让Java语言也可以“甜甜的”。
+ * Hutool最初是我项目中“util”包的一个整理,后来慢慢积累并加入更多非业务相关功能,并广泛学习其它开源项目精髓,经过自己整理修改,最终形成丰富的开源工具集。 + *

+ * + * @author Looly + * + */ +public class Hutool { + + public static final String AUTHOR = "Looly"; + + private Hutool() { + } +} diff --git a/hutool-all/src/main/java/cn/hutool/package-info.java b/hutool-all/src/main/java/cn/hutool/package-info.java new file mode 100644 index 000000000..219d137f4 --- /dev/null +++ b/hutool-all/src/main/java/cn/hutool/package-info.java @@ -0,0 +1,12 @@ +/** + * Hutool是Hu + tool的自造词,前者致敬我的“前任公司”,后者为工具之意,谐音“糊涂”,寓意追求“万事都作糊涂观,无所谓失,无所谓得”的境界。 + * + *

+ * Hutool是一个Java工具包,也只是一个工具包,它帮助我们简化每一行代码,减少每一个方法,让Java语言也可以“甜甜的”。
+ * Hutool最初是我项目中“util”包的一个整理,后来慢慢积累并加入更多非业务相关功能,并广泛学习其它开源项目精髓,经过自己整理修改,最终形成丰富的开源工具集。 + *

+ * + * @author looly + * + */ +package cn.hutool; \ No newline at end of file diff --git a/hutool-aop/pom.xml b/hutool-aop/pom.xml new file mode 100644 index 000000000..18abe31be --- /dev/null +++ b/hutool-aop/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + jar + + + cn.hutool + hutool-parent + 4.6.2-SNAPSHOT + + + hutool-aop + ${project.artifactId} + Hutool 动态代理(AOP) + + + + 3.2.7 + + + + + cn.hutool + hutool-core + ${project.parent.version} + + + cglib + cglib + ${cglib.version} + compile + true + + + diff --git a/hutool-aop/src/main/java/cn/hutool/aop/ProxyUtil.java b/hutool-aop/src/main/java/cn/hutool/aop/ProxyUtil.java new file mode 100644 index 000000000..c539cbeea --- /dev/null +++ b/hutool-aop/src/main/java/cn/hutool/aop/ProxyUtil.java @@ -0,0 +1,73 @@ +package cn.hutool.aop; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Proxy; + +import cn.hutool.aop.aspects.Aspect; +import cn.hutool.aop.proxy.ProxyFactory; +import cn.hutool.core.util.ClassUtil; + +/** + * 代理工具类 + * @author Looly + * + */ +public final class ProxyUtil { + + /** + * 使用切面代理对象 + * + * @param 切面对象类型 + * @param target 目标对象 + * @param aspectClass 切面对象类 + * @return 代理对象 + */ + public static T proxy(T target, Class aspectClass){ + return ProxyFactory.createProxy(target, aspectClass); + } + + /** + * 使用切面代理对象 + * + * @param 被代理对象类型 + * @param aspect 切面对象 + * @return 代理对象 + */ + public static T proxy(T target, Aspect aspect){ + return ProxyFactory.createProxy(target, aspect); + } + + /** + * 创建动态代理对象
+ * 动态代理对象的创建原理是:
+ * 假设创建的代理对象名为 $Proxy0
+ * 1、根据传入的interfaces动态生成一个类,实现interfaces中的接口
+ * 2、通过传入的classloder将刚生成的类加载到jvm中。即将$Proxy0类load
+ * 3、调用$Proxy0的$Proxy0(InvocationHandler)构造函数 创建$Proxy0的对象,并且用interfaces参数遍历其所有接口的方法,这些实现方法的实现本质上是通过反射调用被代理对象的方法
+ * 4、将$Proxy0的实例返回给客户端。
+ * 5、当调用代理类的相应方法时,相当于调用 {@link InvocationHandler#invoke(Object, java.lang.reflect.Method, Object[])} 方法 + * + * + * @param 被代理对象类型 + * @param classloader 被代理类对应的ClassLoader + * @param invocationHandler {@link InvocationHandler} ,被代理类通过实现此接口提供动态代理功能 + * @param interfaces 代理类中需要实现的被代理类的接口方法 + * @return 代理类 + */ + @SuppressWarnings("unchecked") + public static T newProxyInstance(ClassLoader classloader, InvocationHandler invocationHandler, Class... interfaces) { + return (T) Proxy.newProxyInstance(classloader, interfaces, invocationHandler); + } + + /** + * 创建动态代理对象 + * + * @param 被代理对象类型 + * @param invocationHandler {@link InvocationHandler} ,被代理类通过实现此接口提供动态代理功能 + * @param interfaces 代理类中需要实现的被代理类的接口方法 + * @return 代理类 + */ + public static T newProxyInstance(InvocationHandler invocationHandler, Class... interfaces) { + return newProxyInstance(ClassUtil.getClassLoader(), invocationHandler, interfaces); + } +} diff --git a/hutool-aop/src/main/java/cn/hutool/aop/aspects/Aspect.java b/hutool-aop/src/main/java/cn/hutool/aop/aspects/Aspect.java new file mode 100644 index 000000000..ebb020b3f --- /dev/null +++ b/hutool-aop/src/main/java/cn/hutool/aop/aspects/Aspect.java @@ -0,0 +1,43 @@ +package cn.hutool.aop.aspects; + +import java.lang.reflect.Method; + +/** + * 切面接口 + * + * @author looly + * @since 4.18 + */ +public interface Aspect{ + + /** + * 目标方法执行前的操作 + * + * @param target 目标对象 + * @param method 目标方法 + * @param args 参数 + * @return 是否继续执行接下来的操作 + */ + boolean before(Object target, Method method, Object[] args); + + /** + * 目标方法执行后的操作 + * + * @param target 目标对象 + * @param method 目标方法 + * @param args 参数 + * @return 是否允许返回值(接下来的操作) + */ + boolean after(Object target, Method method, Object[] args); + + /** + * 目标方法抛出异常时的操作 + * + * @param target 目标对象 + * @param method 目标方法 + * @param args 参数 + * @param e 异常 + * @return 是否允许抛出异常 + */ + boolean afterException(Object target, Method method, Object[] args, Throwable e); +} diff --git a/hutool-aop/src/main/java/cn/hutool/aop/aspects/SimpleAspect.java b/hutool-aop/src/main/java/cn/hutool/aop/aspects/SimpleAspect.java new file mode 100644 index 000000000..b327e9c02 --- /dev/null +++ b/hutool-aop/src/main/java/cn/hutool/aop/aspects/SimpleAspect.java @@ -0,0 +1,34 @@ +package cn.hutool.aop.aspects; + +import java.io.Serializable; +import java.lang.reflect.Method; + +/** + * 简单切面类,不做任何操作
+ * 可以继承此类实现自己需要的方法即可 + * + * @author Looly + * + */ +public class SimpleAspect implements Aspect, Serializable{ + private static final long serialVersionUID = 1L; + + @Override + public boolean before(Object target, Method method, Object[] args) { + //继承此类后实现此方法 + return true; + } + + @Override + public boolean after(Object target, Method method, Object[] args) { + //继承此类后实现此方法 + return true; + } + + @Override + public boolean afterException(Object target, Method method, Object[] args, Throwable e) { + //继承此类后实现此方法 + return true; + } + +} diff --git a/hutool-aop/src/main/java/cn/hutool/aop/aspects/TimeIntervalAspect.java b/hutool-aop/src/main/java/cn/hutool/aop/aspects/TimeIntervalAspect.java new file mode 100644 index 000000000..4ea3fac91 --- /dev/null +++ b/hutool-aop/src/main/java/cn/hutool/aop/aspects/TimeIntervalAspect.java @@ -0,0 +1,29 @@ +package cn.hutool.aop.aspects; + +import java.lang.reflect.Method; + +import cn.hutool.core.date.TimeInterval; +import cn.hutool.core.lang.Console; + +/** + * 通过日志打印方法的执行时间的切面 + * @author Looly + * + */ +public class TimeIntervalAspect extends SimpleAspect{ + private static final long serialVersionUID = 1L; + + private TimeInterval interval = new TimeInterval(); + + @Override + public boolean before(Object target, Method method, Object[] args) { + interval.start(); + return true; + } + + @Override + public boolean after(Object target, Method method, Object[] args) { + Console.log("Method [{}.{}] execute spend [{}]ms", target.getClass().getName(), method.getName(), interval.intervalMs()); + return true; + } +} diff --git a/hutool-aop/src/main/java/cn/hutool/aop/aspects/package-info.java b/hutool-aop/src/main/java/cn/hutool/aop/aspects/package-info.java new file mode 100644 index 000000000..e70d1f506 --- /dev/null +++ b/hutool-aop/src/main/java/cn/hutool/aop/aspects/package-info.java @@ -0,0 +1,7 @@ +/** + * 切面实现,提供一些基本的切面实现 + * + * @author looly + * + */ +package cn.hutool.aop.aspects; \ No newline at end of file diff --git a/hutool-aop/src/main/java/cn/hutool/aop/interceptor/CglibInterceptor.java b/hutool-aop/src/main/java/cn/hutool/aop/interceptor/CglibInterceptor.java new file mode 100644 index 000000000..b4144753a --- /dev/null +++ b/hutool-aop/src/main/java/cn/hutool/aop/interceptor/CglibInterceptor.java @@ -0,0 +1,60 @@ +package cn.hutool.aop.interceptor; + +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import cn.hutool.aop.aspects.Aspect; +import cn.hutool.core.exceptions.UtilException; +import net.sf.cglib.proxy.MethodInterceptor; +import net.sf.cglib.proxy.MethodProxy; + +/** + * Cglib实现的动态代理切面 + * + * @author looly + * + */ +public class CglibInterceptor implements MethodInterceptor, Serializable { + private static final long serialVersionUID = 1L; + + private Object target; + private Aspect aspect; + + /** + * 构造 + * + * @param target 被代理对象 + * @param aspect 切面实现 + */ + public CglibInterceptor(Object target, Aspect aspect) { + this.target = target; + this.aspect = aspect; + } + + public Object getTarget() { + return this.target; + } + + @Override + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + Object result = null; + if (aspect.before(target, method, args)) { + try { + // result = ReflectUtil.invoke(target, method, args); + result = proxy.invokeSuper(obj, args); + } catch (UtilException e) { + final Throwable cause = e.getCause(); + if (e.getCause() instanceof InvocationTargetException) { + aspect.afterException(target, method, args, ((InvocationTargetException) cause).getTargetException()); + } else { + throw e;// 其它异常属于代理的异常,直接抛出 + } + } + } + if (aspect.after(target, method, args)) { + return result; + } + return null; + } +} diff --git a/hutool-aop/src/main/java/cn/hutool/aop/interceptor/JdkInterceptor.java b/hutool-aop/src/main/java/cn/hutool/aop/interceptor/JdkInterceptor.java new file mode 100644 index 000000000..0969eb11f --- /dev/null +++ b/hutool-aop/src/main/java/cn/hutool/aop/interceptor/JdkInterceptor.java @@ -0,0 +1,62 @@ +package cn.hutool.aop.interceptor; + +import java.io.Serializable; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import cn.hutool.aop.aspects.Aspect; +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.util.ReflectUtil; + +/** + * JDK实现的动态代理切面 + * + * @author Looly + * + */ +public class JdkInterceptor implements InvocationHandler, Serializable{ + private static final long serialVersionUID = 1L; + + private Object target; + private Aspect aspect; + + /** + * 构造 + * + * @param target 被代理对象 + * @param aspect 切面实现 + */ + public JdkInterceptor(Object target, Aspect aspect) { + this.target = target; + this.aspect = aspect; + } + + public Object getTarget() { + return this.target; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + final Object target = this.target; + final Aspect aspect = this.aspect; + Object result = null; + if (aspect.before(target, method, args)) { + try { + result = ReflectUtil.invoke(target, method, args); + } catch (UtilException e) { + final Throwable cause = e.getCause(); + if (e.getCause() instanceof InvocationTargetException) { + aspect.afterException(target, method, args, ((InvocationTargetException) cause).getTargetException()); + } else { + throw e;// 其它异常属于代理的异常,直接抛出 + } + } + } + if (aspect.after(target, method, args)) { + return result; + } + return null; + } + +} diff --git a/hutool-aop/src/main/java/cn/hutool/aop/interceptor/package-info.java b/hutool-aop/src/main/java/cn/hutool/aop/interceptor/package-info.java new file mode 100644 index 000000000..49027f094 --- /dev/null +++ b/hutool-aop/src/main/java/cn/hutool/aop/interceptor/package-info.java @@ -0,0 +1,7 @@ +/** + * 代理拦截器实现 + * + * @author looly + * + */ +package cn.hutool.aop.interceptor; \ No newline at end of file diff --git a/hutool-aop/src/main/java/cn/hutool/aop/package-info.java b/hutool-aop/src/main/java/cn/hutool/aop/package-info.java new file mode 100644 index 000000000..73f90d1b2 --- /dev/null +++ b/hutool-aop/src/main/java/cn/hutool/aop/package-info.java @@ -0,0 +1,7 @@ +/** + * JDK动态代理封装,提供非IOC下的切面支持 + * + * @author looly + * + */ +package cn.hutool.aop; \ No newline at end of file diff --git a/hutool-aop/src/main/java/cn/hutool/aop/proxy/CglibProxyFactory.java b/hutool-aop/src/main/java/cn/hutool/aop/proxy/CglibProxyFactory.java new file mode 100644 index 000000000..1e8ecfe43 --- /dev/null +++ b/hutool-aop/src/main/java/cn/hutool/aop/proxy/CglibProxyFactory.java @@ -0,0 +1,25 @@ +package cn.hutool.aop.proxy; + +import cn.hutool.aop.aspects.Aspect; +import cn.hutool.aop.interceptor.CglibInterceptor; +import net.sf.cglib.proxy.Enhancer; + +/** + * 基于Cglib的切面代理工厂 + * + * @author looly + * + */ +public class CglibProxyFactory extends ProxyFactory{ + private static final long serialVersionUID = 1L; + + @Override + @SuppressWarnings("unchecked") + public T proxy(T target, Aspect aspect) { + final Enhancer enhancer = new Enhancer(); + enhancer.setSuperclass(target.getClass()); + enhancer.setCallback(new CglibInterceptor(target, aspect)); + return (T) enhancer.create(); + } + +} diff --git a/hutool-aop/src/main/java/cn/hutool/aop/proxy/JdkProxyFactory.java b/hutool-aop/src/main/java/cn/hutool/aop/proxy/JdkProxyFactory.java new file mode 100644 index 000000000..d0b53ac4c --- /dev/null +++ b/hutool-aop/src/main/java/cn/hutool/aop/proxy/JdkProxyFactory.java @@ -0,0 +1,21 @@ +package cn.hutool.aop.proxy; + +import cn.hutool.aop.ProxyUtil; +import cn.hutool.aop.aspects.Aspect; +import cn.hutool.aop.interceptor.JdkInterceptor; + +/** + * JDK实现的切面代理 + * + * @author looly + * + */ +public class JdkProxyFactory extends ProxyFactory{ + private static final long serialVersionUID = 1L; + + @Override + @SuppressWarnings("unchecked") + public T proxy(T target, Aspect aspect) { + return (T) ProxyUtil.newProxyInstance(target.getClass().getClassLoader(), new JdkInterceptor(target, aspect), target.getClass().getInterfaces()); + } +} diff --git a/hutool-aop/src/main/java/cn/hutool/aop/proxy/ProxyFactory.java b/hutool-aop/src/main/java/cn/hutool/aop/proxy/ProxyFactory.java new file mode 100644 index 000000000..7e02abb08 --- /dev/null +++ b/hutool-aop/src/main/java/cn/hutool/aop/proxy/ProxyFactory.java @@ -0,0 +1,64 @@ +package cn.hutool.aop.proxy; + +import java.io.Serializable; + +import cn.hutool.aop.aspects.Aspect; +import cn.hutool.core.util.ReflectUtil; + +/** + * 代理工厂
+ * 根据用户引入代理库的不同,产生不同的代理对象 + * + * @author looly + * + */ +public abstract class ProxyFactory implements Serializable{ + private static final long serialVersionUID = 1L; + + /** + * 创建代理 + * + * @param target 被代理对象 + * @param aspect 切面实现 + * @return 代理对象 + */ + public abstract T proxy(T target, Aspect aspect); + + /** + * 根据用户引入Cglib与否自动创建代理对象 + * + * @param 切面对象类型 + * @param target 目标对象 + * @param aspectClass 切面对象类 + * @return 代理对象 + */ + public static T createProxy(T target, Class aspectClass){ + return createProxy(target, ReflectUtil.newInstance(aspectClass)); + } + + /** + * 根据用户引入Cglib与否自动创建代理对象 + * + * @param 切面对象类型 + * @param target 被代理对象 + * @param aspect 切面实现 + * @return 代理对象 + */ + public static T createProxy(T target, Aspect aspect) { + return create().proxy(target, aspect); + } + + /** + * 根据用户引入Cglib与否创建代理工厂 + * + * @return 代理工厂 + */ + public static ProxyFactory create() { + try { + return new CglibProxyFactory(); + } catch (NoClassDefFoundError e) { + // ignore + } + return new JdkProxyFactory(); + } +} diff --git a/hutool-aop/src/main/java/cn/hutool/aop/proxy/package-info.java b/hutool-aop/src/main/java/cn/hutool/aop/proxy/package-info.java new file mode 100644 index 000000000..62915a871 --- /dev/null +++ b/hutool-aop/src/main/java/cn/hutool/aop/proxy/package-info.java @@ -0,0 +1,7 @@ +/** + * 代理实现 + * + * @author looly + * + */ +package cn.hutool.aop.proxy; \ No newline at end of file diff --git a/hutool-aop/src/test/java/cn/hutool/aop/test/AopTest.java b/hutool-aop/src/test/java/cn/hutool/aop/test/AopTest.java new file mode 100644 index 000000000..31f12325a --- /dev/null +++ b/hutool-aop/src/test/java/cn/hutool/aop/test/AopTest.java @@ -0,0 +1,60 @@ +package cn.hutool.aop.test; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.aop.ProxyUtil; +import cn.hutool.aop.aspects.TimeIntervalAspect; + +/** + * AOP模块单元测试 + * + * @author Looly + * + */ +public class AopTest { + + @Test + public void aopTest() { + Animal cat = ProxyUtil.proxy(new Cat(), TimeIntervalAspect.class); + String result = cat.eat(); + Assert.assertEquals("猫吃鱼", result); + } + + @Test + public void aopByCglibTest() { + Dog dog = ProxyUtil.proxy(new Dog(), TimeIntervalAspect.class); + String result = dog.eat(); + Assert.assertEquals("狗吃肉", result); + } + + static interface Animal { + String eat(); + } + + /** + * 有接口 + * + * @author looly + * + */ + static class Cat implements Animal { + + @Override + public String eat() { + return "猫吃鱼"; + } + } + + /** + * 无接口 + * + * @author looly + * + */ + static class Dog { + public String eat() { + return "狗吃肉"; + } + } +} diff --git a/hutool-bloomFilter/pom.xml b/hutool-bloomFilter/pom.xml new file mode 100644 index 000000000..8c2efa899 --- /dev/null +++ b/hutool-bloomFilter/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + jar + + + cn.hutool + hutool-parent + 4.6.2-SNAPSHOT + + + hutool-bloomFilter + ${project.artifactId} + Hutool 布隆过滤器 + + + + cn.hutool + hutool-core + ${project.parent.version} + + + diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BitMapBloomFilter.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BitMapBloomFilter.java new file mode 100644 index 000000000..ada7b4cd4 --- /dev/null +++ b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BitMapBloomFilter.java @@ -0,0 +1,79 @@ +package cn.hutool.bloomfilter; + +import cn.hutool.bloomfilter.filter.DefaultFilter; +import cn.hutool.bloomfilter.filter.ELFFilter; +import cn.hutool.bloomfilter.filter.JSFilter; +import cn.hutool.bloomfilter.filter.PJWFilter; +import cn.hutool.bloomfilter.filter.SDBMFilter; +import cn.hutool.core.util.NumberUtil; + +/** + * BlommFilter 实现
+ * 1.构建hash算法
+ * 2.散列hash映射到数组的bit位置
+ * 3.验证
+ * 此实现方式可以指定Hash算法 + * + * @author Ansj + */ +public class BitMapBloomFilter implements BloomFilter{ + private static final long serialVersionUID = 1L; + + private BloomFilter[] filters; + + /** + * 构造,使用默认的5个过滤器 + * @param m M值决定BitMap的大小 + */ + public BitMapBloomFilter(int m) { + int mNum =NumberUtil.div(String.valueOf(m), String.valueOf(5)).intValue(); + long size = (long) (1L * mNum * 1024 * 1024 * 8); + + filters = new BloomFilter[]{ + new DefaultFilter(size), + new ELFFilter(size), + new JSFilter(size), + new PJWFilter(size), + new SDBMFilter(size) + }; + } + + /** + * 使用自定的多个过滤器建立BloomFilter + * + * @param m M值决定BitMap的大小 + * @param filters Bloom过滤器列表 + */ + public BitMapBloomFilter(int m, BloomFilter... filters) { + this(m); + this.filters = filters; + } + + /** + * 增加字符串到Filter映射中 + * @param str 字符串 + */ + @Override + public boolean add(String str) { + boolean flag = true; + for (BloomFilter filter : filters) { + flag |= filter.add(str); + } + return flag; + } + + /** + * 是否可能包含此字符串,此处存在误判 + * @param str 字符串 + * @return 是否存在 + */ + @Override + public boolean contains(String str) { + for (BloomFilter filter : filters) { + if (filter.contains(str) == false) { + return false; + } + } + return true; + } +} \ No newline at end of file diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BitSetBloomFilter.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BitSetBloomFilter.java new file mode 100644 index 000000000..81e591da4 --- /dev/null +++ b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BitSetBloomFilter.java @@ -0,0 +1,145 @@ +package cn.hutool.bloomfilter; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.BitSet; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.HashUtil; + +/** + * BloomFilter实现方式2,此方式使用BitSet存储。
+ * Hash算法的使用使用固定顺序,只需指定个数既可 + * @author loolly + * + */ +public class BitSetBloomFilter implements BloomFilter{ + private static final long serialVersionUID = 1L; + + private BitSet bitSet; + private int bitSetSize; + private int addedElements; + private int hashFunctionNumber; + + /** + * 构造一个布隆过滤器,过滤器的容量为c * n 个bit. + * + * @param c 当前过滤器预先开辟的最大包含记录,通常要比预计存入的记录多一倍. + * @param n 当前过滤器预计所要包含的记录. + * @param k 哈希函数的个数,等同每条记录要占用的bit数. + */ + public BitSetBloomFilter(int c, int n, int k) { + this.hashFunctionNumber = k; + this.bitSetSize = (int) Math.ceil(c * k); + this.addedElements = n; + this.bitSet = new BitSet(this.bitSetSize); + } + + /** + * 通过文件初始化过滤器. + * + * @param path 文件路径 + * @param charset 字符集 + * @throws IOException IO异常 + */ + public void init(String path, String charset) throws IOException { + BufferedReader reader = FileUtil.getReader(path, charset); + try { + String line; + while(true) { + line = reader.readLine(); + if(line == null) { + break; + } + this.add(line); + } + }finally { + IoUtil.close(reader); + } + } + + @Override + public boolean add(String str) { + if (contains(str)) { + return false; + } + + int[] positions = createHashes(str, hashFunctionNumber); + for (int i = 0; i < positions.length; i++) { + int position = Math.abs(positions[i] % bitSetSize); + bitSet.set(position, true); + } + return true; + } + + /** + * 判定是否包含指定字符串 + * @param str 字符串 + * @return 是否包含,存在误差 + */ + @Override + public boolean contains(String str) { + int[] positions = createHashes(str, hashFunctionNumber); + for (int i : positions) { + int position = Math.abs(i % bitSetSize); + if (!bitSet.get(position)) { + return false; + } + } + return true; + } + + /** + * @return 得到当前过滤器的错误率. + */ + public double getFalsePositiveProbability() { + // (1 - e^(-k * n / m)) ^ k + return Math.pow((1 - Math.exp(-hashFunctionNumber * (double) addedElements / bitSetSize)), hashFunctionNumber); + } + + /** + * 将字符串的字节表示进行多哈希编码. + * + * @param str 待添加进过滤器的字符串字节表示. + * @param hashNumber 要经过的哈希个数. + * @return 各个哈希的结果数组. + */ + public static int[] createHashes(String str, int hashNumber) { + int[] result = new int[hashNumber]; + for(int i = 0; i < hashNumber; i++) { + result[i] = hash(str, i); + + } + return result; + } + + /** + * 计算Hash值 + * @param str 被计算Hash的字符串 + * @param k Hash算法序号 + * @return Hash值 + */ + public static int hash(String str, int k) { + switch (k) { + case 0: + return HashUtil.rsHash(str); + case 1: + return HashUtil.jsHash(str); + case 2: + return HashUtil.elfHash(str); + case 3: + return HashUtil.bkdrHash(str); + case 4: + return HashUtil.apHash(str); + case 5: + return HashUtil.djbHash(str); + case 6: + return HashUtil.sdbmHash(str); + case 7: + return HashUtil.pjwHash(str); + default: + return 0; + } + } +} \ No newline at end of file diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BloomFilter.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BloomFilter.java new file mode 100644 index 000000000..0086bf4ad --- /dev/null +++ b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BloomFilter.java @@ -0,0 +1,29 @@ +package cn.hutool.bloomfilter; + +import java.io.Serializable; + +/** + * Bloom filter 是由 Howard Bloom 在 1970 年提出的二进制向量数据结构,它具有很好的空间和时间效率,被用来检测一个元素是不是集合中的一个成员。
+ * 如果检测结果为是,该元素不一定在集合中;但如果检测结果为否,该元素一定不在集合中。
+ * 因此Bloom filter具有100%的召回率。这样每个检测请求返回有“在集合内(可能错误)”和“不在集合内(绝对不在集合内)”两种情况。
+ * @author Looly + * + */ +public interface BloomFilter extends Serializable{ + + /** + * + * @param str 字符串 + * @return 判断一个字符串是否bitMap中存在 + */ + public boolean contains(String str); + + /** + * 在boolean的bitMap中增加一个字符串
+ * 如果存在就返回false .如果不存在.先增加这个字符串.再返回true + * + * @param str 字符串 + * @return 是否加入成功,如果存在就返回false .如果不存在返回true + */ + public boolean add(String str); +} \ No newline at end of file diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BloomFilterUtil.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BloomFilterUtil.java new file mode 100644 index 000000000..990438330 --- /dev/null +++ b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/BloomFilterUtil.java @@ -0,0 +1,32 @@ +package cn.hutool.bloomfilter; + +/** + * 布隆过滤器工具 + * + * @author looly + * @since 4.1.5 + */ +public class BloomFilterUtil { + + /** + * 创建一个BitSet实现的布隆过滤器,过滤器的容量为c * n 个bit. + * + * @param c 当前过滤器预先开辟的最大包含记录,通常要比预计存入的记录多一倍. + * @param n 当前过滤器预计所要包含的记录. + * @param k 哈希函数的个数,等同每条记录要占用的bit数. + * @return BitSetBloomFilter + */ + public static BitSetBloomFilter createBitSet(int c, int n, int k) { + return new BitSetBloomFilter(c, n, k); + } + + /** + * 创建BitMap实现的布隆过滤器 + * + * @param m BitMap的大小 + * @return BitMapBloomFilter + */ + public static BitMapBloomFilter createBitMap(int m) { + return new BitMapBloomFilter(m); + } +} diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/BitMap.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/BitMap.java new file mode 100644 index 000000000..9909f8a69 --- /dev/null +++ b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/BitMap.java @@ -0,0 +1,34 @@ +package cn.hutool.bloomfilter.bitMap; + +/** + * BitMap接口,用于将某个int或long值映射到一个数组中,从而判定某个值是否存在 + * + * @author looly + * + */ +public interface BitMap{ + + public final int MACHINE32 = 32; + public final int MACHINE64 = 64; + + /** + * 加入值 + * + * @param i 值 + */ + public void add(long i); + + /** + * 检查是否包含值 + * + * @param i 值 + */ + public boolean contains(long i); + + /** + * 移除值 + * + * @param i 值 + */ + public void remove(long i); +} \ No newline at end of file diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/IntMap.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/IntMap.java new file mode 100644 index 000000000..0ced56cb8 --- /dev/null +++ b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/IntMap.java @@ -0,0 +1,56 @@ +package cn.hutool.bloomfilter.bitMap; + +import java.io.Serializable; + +/** + * 过滤器BitMap在32位机器上.这个类能发生更好的效果.一般情况下建议使用此类 + * + * @author loolly + * + */ +public class IntMap implements BitMap, Serializable { + private static final long serialVersionUID = 1L; + + private int[] ints = null; + + /** + * 构造 + */ + public IntMap() { + ints = new int[93750000]; + } + + /** + * 构造 + * + * @param size 容量 + */ + public IntMap(int size) { + ints = new int[size]; + } + + @Override + public void add(long i) { + int r = (int) (i / BitMap.MACHINE32); + int c = (int) (i % BitMap.MACHINE32); + ints[r] = (int) (ints[r] | (1 << c)); + } + + @Override + public boolean contains(long i) { + int r = (int) (i / BitMap.MACHINE32); + int c = (int) (i % BitMap.MACHINE32); + if (((int) ((ints[r] >>> c)) & 1) == 1) { + return true; + } + return false; + } + + @Override + public void remove(long i) { + int r = (int) (i / BitMap.MACHINE32); + int c = (int) (i % BitMap.MACHINE32); + ints[r] &= ~(1 << c); + } + +} \ No newline at end of file diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/LongMap.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/LongMap.java new file mode 100644 index 000000000..6a90940fd --- /dev/null +++ b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/LongMap.java @@ -0,0 +1,56 @@ +package cn.hutool.bloomfilter.bitMap; + +import java.io.Serializable; + +/** + * 过滤器BitMap在64位机器上.这个类能发生更好的效果.一般机器不建议使用 + * + * @author loolly + * + */ +public class LongMap implements BitMap, Serializable { + private static final long serialVersionUID = 1L; + + private long[] longs = null; + + /** + * 构造 + */ + public LongMap() { + longs = new long[93750000]; + } + + /** + * 构造 + * + * @param size 容量 + */ + public LongMap(int size) { + longs = new long[size]; + } + + @Override + public void add(long i) { + int r = (int) (i / BitMap.MACHINE64); + long c = i % BitMap.MACHINE64; + longs[r] = longs[r] | (1 << c); + } + + @Override + public boolean contains(long i) { + int r = (int) (i / BitMap.MACHINE64); + long c = i % BitMap.MACHINE64; + if (((longs[r] >>> c) & 1) == 1) { + return true; + } + return false; + } + + @Override + public void remove(long i) { + int r = (int) (i / BitMap.MACHINE64); + long c = i % BitMap.MACHINE64; + longs[r] &= ~(1 << c); + } + +} \ No newline at end of file diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/package-info.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/package-info.java new file mode 100644 index 000000000..47ec310dc --- /dev/null +++ b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/package-info.java @@ -0,0 +1,7 @@ +/** + * BitMap实现 + * + * @author looly + * + */ +package cn.hutool.bloomfilter.bitMap; \ No newline at end of file diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/AbstractFilter.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/AbstractFilter.java new file mode 100644 index 000000000..b1ac316e9 --- /dev/null +++ b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/AbstractFilter.java @@ -0,0 +1,83 @@ +package cn.hutool.bloomfilter.filter; + +import cn.hutool.bloomfilter.BloomFilter; +import cn.hutool.bloomfilter.bitMap.BitMap; +import cn.hutool.bloomfilter.bitMap.IntMap; +import cn.hutool.bloomfilter.bitMap.LongMap; + +/** + * 抽象Bloom过滤器 + * + * @author loolly + * + */ +public abstract class AbstractFilter implements BloomFilter { + private static final long serialVersionUID = 1L; + + private BitMap bm = null; + + protected long size = 0; + + /** + * 构造 + * + * @param maxValue 最大值 + * @param machineNum 机器位数 + */ + public AbstractFilter(long maxValue, int machineNum) { + init(maxValue, machineNum); + } + + /** + * 构造32位 + * + * @param maxValue 最大值 + */ + public AbstractFilter(long maxValue) { + this(maxValue, BitMap.MACHINE32); + } + + /** + * 初始化 + * + * @param maxValue 最大值 + * @param machineNum 机器位数 + */ + public void init(long maxValue, int machineNum) { + this.size = maxValue; + switch (machineNum) { + case BitMap.MACHINE32: + bm = new IntMap((int) (size / machineNum)); + break; + case BitMap.MACHINE64: + bm = new LongMap((int) (size / machineNum)); + break; + default: + throw new RuntimeException("Error Machine number!"); + } + } + + @Override + public boolean contains(String str) { + return bm.contains(Math.abs(hash(str))); + } + + @Override + public boolean add(String str) { + final long hash = Math.abs(hash(str)); + if (bm.contains(hash)) { + return false; + } + + bm.add(hash); + return true; + } + + /** + * 自定义Hash方法 + * + * @param str 字符串 + * @return HashCode + */ + public abstract long hash(String str); +} \ No newline at end of file diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/DefaultFilter.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/DefaultFilter.java new file mode 100644 index 000000000..edb116fa0 --- /dev/null +++ b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/DefaultFilter.java @@ -0,0 +1,25 @@ +package cn.hutool.bloomfilter.filter; + +import cn.hutool.core.util.HashUtil; + +/** + * 默认Bloom过滤器,使用Java自带的Hash算法 + * @author loolly + * + */ +public class DefaultFilter extends AbstractFilter { + private static final long serialVersionUID = 1L; + + public DefaultFilter(long maxValue, int MACHINENUM) { + super(maxValue, MACHINENUM); + } + + public DefaultFilter(long maxValue) { + super(maxValue); + } + + @Override + public long hash(String str) { + return HashUtil.javaDefaultHash(str) % size; + } +} diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/ELFFilter.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/ELFFilter.java new file mode 100644 index 000000000..e7c2a5f0c --- /dev/null +++ b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/ELFFilter.java @@ -0,0 +1,21 @@ +package cn.hutool.bloomfilter.filter; + +import cn.hutool.core.util.HashUtil; + +public class ELFFilter extends AbstractFilter { + private static final long serialVersionUID = 1L; + + public ELFFilter(long maxValue, int MACHINENUM) { + super(maxValue, MACHINENUM); + } + + public ELFFilter(long maxValue) { + super(maxValue); + } + + @Override + public long hash(String str) { + return HashUtil.elfHash(str) % size; + } + +} diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/FNVFilter.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/FNVFilter.java new file mode 100644 index 000000000..457d1219e --- /dev/null +++ b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/FNVFilter.java @@ -0,0 +1,21 @@ +package cn.hutool.bloomfilter.filter; + +import cn.hutool.core.util.HashUtil; + +public class FNVFilter extends AbstractFilter { + private static final long serialVersionUID = 1L; + + public FNVFilter(long maxValue, int machineNum) { + super(maxValue, machineNum); + } + + public FNVFilter(long maxValue) { + super(maxValue); + } + + @Override + public long hash(String str) { + return HashUtil.fnvHash(str); + } + +} diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/HfFilter.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/HfFilter.java new file mode 100644 index 000000000..e3eb8769a --- /dev/null +++ b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/HfFilter.java @@ -0,0 +1,31 @@ +package cn.hutool.bloomfilter.filter; + + +public class HfFilter extends AbstractFilter { + private static final long serialVersionUID = 1L; + + public HfFilter(long maxValue, int machineNum) { + super(maxValue, machineNum); + } + + public HfFilter(long maxValue) { + super(maxValue); + } + + @Override + public long hash(String str) { + int length = str.length() ; + long hash = 0; + + for (int i = 0; i < length; i++) { + hash += str.charAt(i) * 3 * i; + } + + if (hash < 0) { + hash = -hash; + } + + return hash % size; + } + +} diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/HfIpFilter.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/HfIpFilter.java new file mode 100644 index 000000000..d25b2daab --- /dev/null +++ b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/HfIpFilter.java @@ -0,0 +1,24 @@ +package cn.hutool.bloomfilter.filter; + +public class HfIpFilter extends AbstractFilter { + private static final long serialVersionUID = 1L; + + public HfIpFilter(long maxValue, int machineNum) { + super(maxValue, machineNum); + } + + public HfIpFilter(long maxValue) { + super(maxValue); + } + + @Override + public long hash(String str) { + int length = str.length(); + long hash = 0; + for (int i = 0; i < length; i++) { + hash += str.charAt(i % 4) ^ str.charAt(i); + } + return hash % size; + } + +} diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/JSFilter.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/JSFilter.java new file mode 100644 index 000000000..0cadea70e --- /dev/null +++ b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/JSFilter.java @@ -0,0 +1,30 @@ +package cn.hutool.bloomfilter.filter; + + +public class JSFilter extends AbstractFilter { + private static final long serialVersionUID = 1L; + + public JSFilter(long maxValue, int machineNum) { + super(maxValue, machineNum); + } + + public JSFilter(long maxValue) { + super(maxValue); + } + + @Override + public long hash(String str) { + int hash = 1315423911; + + for (int i = 0; i < str.length(); i++) { + hash ^= ((hash << 5) + str.charAt(i) + (hash >> 2)); + } + + if(hash<0) { + hash*=-1 ; + } + + return hash % size; + } + +} diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/PJWFilter.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/PJWFilter.java new file mode 100644 index 000000000..6f57c4daf --- /dev/null +++ b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/PJWFilter.java @@ -0,0 +1,21 @@ +package cn.hutool.bloomfilter.filter; + +import cn.hutool.core.util.HashUtil; + +public class PJWFilter extends AbstractFilter { + private static final long serialVersionUID = 1L; + + public PJWFilter(long maxValue, int machineNum) { + super(maxValue, machineNum); + } + + public PJWFilter(long maxValue) { + super(maxValue); + } + + @Override + public long hash(String str) { + return HashUtil.pjwHash(str) % size; + } + +} diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/RSFilter.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/RSFilter.java new file mode 100644 index 000000000..c8a357b0d --- /dev/null +++ b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/RSFilter.java @@ -0,0 +1,21 @@ +package cn.hutool.bloomfilter.filter; + +import cn.hutool.core.util.HashUtil; + +public class RSFilter extends AbstractFilter { + private static final long serialVersionUID = 1L; + + public RSFilter(long maxValue, int machineNum) { + super(maxValue, machineNum); + } + + public RSFilter(long maxValue) { + super(maxValue); + } + + @Override + public long hash(String str) { + return HashUtil.rsHash(str) % size; + } + +} diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/SDBMFilter.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/SDBMFilter.java new file mode 100644 index 000000000..5225841a8 --- /dev/null +++ b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/SDBMFilter.java @@ -0,0 +1,21 @@ +package cn.hutool.bloomfilter.filter; + +import cn.hutool.core.util.HashUtil; + +public class SDBMFilter extends AbstractFilter { + private static final long serialVersionUID = 1L; + + public SDBMFilter(long maxValue, int machineNum) { + super(maxValue, machineNum); + } + + public SDBMFilter(long maxValue) { + super(maxValue); + } + + @Override + public long hash(String str) { + return HashUtil.sdbmHash(str) % size; + } + +} diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/TianlFilter.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/TianlFilter.java new file mode 100644 index 000000000..d9ff5a086 --- /dev/null +++ b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/TianlFilter.java @@ -0,0 +1,22 @@ +package cn.hutool.bloomfilter.filter; + +import cn.hutool.core.util.HashUtil; + + +public class TianlFilter extends AbstractFilter { + private static final long serialVersionUID = 1L; + + public TianlFilter(long maxValue, int machineNum) { + super(maxValue, machineNum); + } + + public TianlFilter(long maxValue) { + super(maxValue); + } + + @Override + public long hash(String str) { + return HashUtil.tianlHash(str) % size; + } + +} diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/package-info.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/package-info.java new file mode 100644 index 000000000..7fa569d0a --- /dev/null +++ b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/package-info.java @@ -0,0 +1,7 @@ +/** + * 各种Hash算法的过滤器实现 + * + * @author looly + * + */ +package cn.hutool.bloomfilter.filter; \ No newline at end of file diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/package-info.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/package-info.java new file mode 100644 index 000000000..84db849ce --- /dev/null +++ b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/package-info.java @@ -0,0 +1,7 @@ +/** + * 布隆过滤,提供一些Hash算法的布隆过滤 + * + * @author looly + * + */ +package cn.hutool.bloomfilter; \ No newline at end of file diff --git a/hutool-bloomFilter/src/test/java/cn/hutool/bloomfilter/BitMapBloomFilterTest.java b/hutool-bloomFilter/src/test/java/cn/hutool/bloomfilter/BitMapBloomFilterTest.java new file mode 100644 index 000000000..0463d2503 --- /dev/null +++ b/hutool-bloomFilter/src/test/java/cn/hutool/bloomfilter/BitMapBloomFilterTest.java @@ -0,0 +1,55 @@ +package cn.hutool.bloomfilter; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.bloomfilter.bitMap.IntMap; +import cn.hutool.bloomfilter.bitMap.LongMap; + +public class BitMapBloomFilterTest { + + @Test + public void filterTest() { + BitMapBloomFilter filter = new BitMapBloomFilter(10); + filter.add("123"); + filter.add("abc"); + filter.add("ddd"); + + Assert.assertTrue(filter.contains("abc")); + Assert.assertTrue(filter.contains("ddd")); + Assert.assertTrue(filter.contains("123")); + } + + @Test + @Ignore + public void testIntMap(){ + IntMap intMap = new IntMap(); + + for (int i = 0 ; i < 32; i++) { + intMap.add(i); + } + intMap.remove(30); + + + for (int i = 0; i < 32; i++) { + System.out.println(i + "是否存在-->" + intMap.contains(i)); + } + } + + @Test + @Ignore + public void testLongMap(){ + LongMap longMap = new LongMap(); + + for (int i = 0 ; i < 64; i++) { + longMap.add(i); + } + longMap.remove(30); + + + for (int i = 0; i < 64; i++) { + System.out.println(i + "是否存在-->" + longMap.contains(i)); + } + } +} diff --git a/hutool-bom/pom.xml b/hutool-bom/pom.xml new file mode 100644 index 000000000..cb77ee5c8 --- /dev/null +++ b/hutool-bom/pom.xml @@ -0,0 +1,111 @@ + + + 4.0.0 + + pom + + + cn.hutool + hutool-parent + 4.6.2-SNAPSHOT + + + hutool-bom + ${project.artifactId} + 提供丰富的Java工具方法,此模块为Hutool所有模块汇总,最终形式为拆分开的多个jar包,可以通过exclude方式排除不需要的模块 + https://github.com/looly/hutool + + + + cn.hutool + hutool-core + ${project.parent.version} + + + cn.hutool + hutool-aop + ${project.parent.version} + + + cn.hutool + hutool-bloomFilter + ${project.parent.version} + + + cn.hutool + hutool-cache + ${project.parent.version} + + + cn.hutool + hutool-crypto + ${project.parent.version} + + + cn.hutool + hutool-db + ${project.parent.version} + + + cn.hutool + hutool-dfa + ${project.parent.version} + + + cn.hutool + hutool-extra + ${project.parent.version} + + + cn.hutool + hutool-http + ${project.parent.version} + + + cn.hutool + hutool-log + ${project.parent.version} + + + cn.hutool + hutool-script + ${project.parent.version} + + + cn.hutool + hutool-setting + ${project.parent.version} + + + cn.hutool + hutool-system + ${project.parent.version} + + + cn.hutool + hutool-cron + ${project.parent.version} + + + cn.hutool + hutool-json + ${project.parent.version} + + + cn.hutool + hutool-poi + ${project.parent.version} + + + cn.hutool + hutool-captcha + ${project.parent.version} + + + cn.hutool + hutool-socket + ${project.parent.version} + + + + diff --git a/hutool-cache/pom.xml b/hutool-cache/pom.xml new file mode 100644 index 000000000..2687bb958 --- /dev/null +++ b/hutool-cache/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + jar + + + cn.hutool + hutool-parent + 4.6.2-SNAPSHOT + + + hutool-cache + ${project.artifactId} + Hutool 缓存 + + + + cn.hutool + hutool-core + ${project.parent.version} + + + diff --git a/hutool-cache/src/main/java/cn/hutool/cache/Cache.java b/hutool-cache/src/main/java/cn/hutool/cache/Cache.java new file mode 100644 index 000000000..d9b23b29b --- /dev/null +++ b/hutool-cache/src/main/java/cn/hutool/cache/Cache.java @@ -0,0 +1,149 @@ +package cn.hutool.cache; + +import java.io.Serializable; +import java.util.Iterator; + +import cn.hutool.cache.impl.CacheObj; +import cn.hutool.core.lang.func.Func0; + +/** + * 缓存接口 + * + * @author Looly,jodd + * + * @param 键类型 + * @param 值类型 + */ +public interface Cache extends Iterable, Serializable { + + /** + * 返回缓存容量,0表示无大小限制 + * + * @return 返回缓存容量,0表示无大小限制 + */ + int capacity(); + + /** + * 缓存失效时长, 0 表示没有设置,单位毫秒 + * + * @return 缓存失效时长, 0 表示没有设置,单位毫秒 + */ + long timeout(); + + /** + * 将对象加入到缓存,使用默认失效时长 + * + * @param key 键 + * @param object 缓存的对象 + * @see Cache#put(Object, Object, long) + */ + void put(K key, V object); + + /** + * 将对象加入到缓存,使用指定失效时长
+ * 如果缓存空间满了,{@link #prune()} 将被调用以获得空间来存放新对象 + * + * @param key 键 + * @param object 缓存的对象 + * @param timeout 失效时长,单位毫秒 + * @see Cache#put(Object, Object, long) + */ + void put(K key, V object, long timeout); + + /** + * 从缓存中获得对象,当对象不在缓存中或已经过期返回null + *

+ * 调用此方法时,会检查上次调用时间,如果与当前时间差值大于超时时间返回null,否则返回值。 + *

+ * 每次调用此方法会刷新最后访问时间,也就是说会重新计算超时时间。 + * + * @param key 键 + * @return 键对应的对象 + * @see #get(Object, boolean) + */ + V get(K key); + + /** + * 从缓存中获得对象,当对象不在缓存中或已经过期返回Func0回调产生的对象 + * + * @param key 键 + * @param supplier 如果不存在回调方法,用于生产值对象 + * @return 值对象 + */ + V get(K key, Func0 supplier); + + /** + * 从缓存中获得对象,当对象不在缓存中或已经过期返回null + *

+ * 调用此方法时,会检查上次调用时间,如果与当前时间差值大于超时时间返回null,否则返回值。 + * + * @param key 键 + * @param isUpdateLastAccess 是否更新最后访问时间,即重新计算超时时间。 + * @return 键对应的对象 + */ + V get(K key, boolean isUpdateLastAccess); + + /** + * 返回缓存迭代器 + * + * @return 缓存迭代器 + */ + @Override + Iterator iterator(); + + /** + * 返回包含键和值得迭代器 + * + * @return 缓存对象迭代器 + * @since 4.0.10 + */ + Iterator> cacheObjIterator(); + + /** + * 从缓存中清理过期对象,清理策略取决于具体实现 + * + * @return 清理的缓存对象个数 + */ + int prune(); + + /** + * 缓存是否已满,仅用于有空间限制的缓存对象 + * + * @return 缓存是否已满,仅用于有空间限制的缓存对象 + */ + boolean isFull(); + + /** + * 从缓存中移除对象 + * + * @param key 键 + */ + void remove(K key); + + /** + * 清空缓存 + */ + void clear(); + + /** + * 缓存的对象数量 + * + * @return 缓存的对象数量 + */ + int size(); + + /** + * 缓存是否为空 + * + * @return 缓存是否为空 + */ + boolean isEmpty(); + + /** + * 是否包含key + * + * @param key KEY + * @return 是否包含key + */ + boolean containsKey(K key); +} diff --git a/hutool-cache/src/main/java/cn/hutool/cache/CacheUtil.java b/hutool-cache/src/main/java/cn/hutool/cache/CacheUtil.java new file mode 100644 index 000000000..cac25c555 --- /dev/null +++ b/hutool-cache/src/main/java/cn/hutool/cache/CacheUtil.java @@ -0,0 +1,129 @@ +package cn.hutool.cache; + +import cn.hutool.cache.impl.FIFOCache; +import cn.hutool.cache.impl.LFUCache; +import cn.hutool.cache.impl.LRUCache; +import cn.hutool.cache.impl.NoCache; +import cn.hutool.cache.impl.TimedCache; +import cn.hutool.cache.impl.WeakCache; + +/** + * 缓存工具类 + * @author Looly + *@since 3.0.1 + */ +public class CacheUtil { + + /** + * 创建FIFO(first in first out) 先进先出缓存. + * + * @param Key类型 + * @param Value类型 + * @param capacity 容量 + * @param timeout 过期时长,单位:毫秒 + * @return {@link FIFOCache} + */ + public static FIFOCache newFIFOCache(int capacity, long timeout){ + return new FIFOCache(capacity, timeout); + } + + /** + * 创建FIFO(first in first out) 先进先出缓存. + * + * @param Key类型 + * @param Value类型 + * @param capacity 容量 + * @return {@link FIFOCache} + */ + public static FIFOCache newFIFOCache(int capacity){ + return new FIFOCache(capacity); + } + + /** + * 创建LFU(least frequently used) 最少使用率缓存. + * + * @param Key类型 + * @param Value类型 + * @param capacity 容量 + * @param timeout 过期时长,单位:毫秒 + * @return {@link LFUCache} + */ + public static LFUCache newLFUCache(int capacity, long timeout){ + return new LFUCache(capacity, timeout); + } + + /** + * 创建LFU(least frequently used) 最少使用率缓存. + * + * @param Key类型 + * @param Value类型 + * @param capacity 容量 + * @return {@link LFUCache} + */ + public static LFUCache newLFUCache(int capacity){ + return new LFUCache(capacity); + } + + + /** + * 创建LRU (least recently used)最近最久未使用缓存. + * + * @param Key类型 + * @param Value类型 + * @param capacity 容量 + * @param timeout 过期时长,单位:毫秒 + * @return {@link LRUCache} + */ + public static LRUCache newLRUCache(int capacity, long timeout){ + return new LRUCache(capacity, timeout); + } + + /** + * 创建LRU (least recently used)最近最久未使用缓存. + * + * @param Key类型 + * @param Value类型 + * @param capacity 容量 + * @return {@link LRUCache} + */ + public static LRUCache newLRUCache(int capacity){ + return new LRUCache(capacity); + } + + /** + * 创建定时缓存. + * + * @param Key类型 + * @param Value类型 + * @param timeout 过期时长,单位:毫秒 + * @return {@link TimedCache} + */ + public static TimedCache newTimedCache(long timeout){ + return new TimedCache(timeout); + } + + /** + * 创建弱引用缓存. + * + * @param Key类型 + * @param Value类型 + * @param timeout 过期时长,单位:毫秒 + * @return {@link WeakCache} + * @since 3.0.7 + */ + public static WeakCache newWeakCache(long timeout){ + return new WeakCache(timeout); + } + + /** + * 创建无缓存实现. + * + * @param Key类型 + * @param Value类型 + * @return {@link NoCache} + */ + public static NoCache newNoCache(){ + return new NoCache(); + } + +} diff --git a/hutool-cache/src/main/java/cn/hutool/cache/GlobalPruneTimer.java b/hutool-cache/src/main/java/cn/hutool/cache/GlobalPruneTimer.java new file mode 100644 index 000000000..d2efb8ce1 --- /dev/null +++ b/hutool-cache/src/main/java/cn/hutool/cache/GlobalPruneTimer.java @@ -0,0 +1,83 @@ +package cn.hutool.cache; + +import java.util.List; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 全局缓存清理定时器池,用于在需要过期支持的缓存对象中超时任务池 + * + * @author looly + * + */ +public enum GlobalPruneTimer { + /** 单例对象 */ + INSTANCE; + + /** 缓存任务计数 */ + private AtomicInteger cacheTaskNumber = new AtomicInteger(1); + + /** 定时器 */ + private ScheduledExecutorService pruneTimer; + + /** + * 构造 + */ + private GlobalPruneTimer() { + create(); + } + + /** + * 启动定时任务 + * + * @param task 任务 + * @param delay 周期 + * @return {@link ScheduledFuture}对象,可手动取消此任务 + */ + public ScheduledFuture schedule(Runnable task, long delay) { + return this.pruneTimer.scheduleAtFixedRate(task, delay, delay, TimeUnit.MILLISECONDS); + } + + /** + * 创建定时器 + */ + public void create() { + if (null != pruneTimer) { + shutdownNow(); + } + this.pruneTimer = new ScheduledThreadPoolExecutor(16, new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return ThreadUtil.newThread(r, StrUtil.format("Pure-Timer-{}", cacheTaskNumber.getAndIncrement())); + } + }); + } + + /** + * 销毁全局定时器 + */ + public void shutdown() { + if (null != pruneTimer) { + pruneTimer.shutdown(); + } + } + + /** + * 销毁全局定时器 + * + * @return 销毁时未被执行的任务列表 + */ + public List shutdownNow() { + if (null != pruneTimer) { + return pruneTimer.shutdownNow(); + } + return null; + } +} diff --git a/hutool-cache/src/main/java/cn/hutool/cache/file/AbstractFileCache.java b/hutool-cache/src/main/java/cn/hutool/cache/file/AbstractFileCache.java new file mode 100644 index 000000000..7f0e859a4 --- /dev/null +++ b/hutool-cache/src/main/java/cn/hutool/cache/file/AbstractFileCache.java @@ -0,0 +1,134 @@ +package cn.hutool.cache.file; + +import java.io.File; +import java.io.Serializable; + +import cn.hutool.cache.Cache; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; + +/** + * 文件缓存,以解决频繁读取文件引起的性能问题 + * @author Looly + * + */ +public abstract class AbstractFileCache implements Serializable{ + private static final long serialVersionUID = 1L; + + /** 容量 */ + protected final int capacity; + /** 缓存的最大文件大小,文件大于此大小时将不被缓存 */ + protected final int maxFileSize; + /** 默认超时时间,0表示无默认超时 */ + protected final long timeout; + /** 缓存实现 */ + protected final Cache cache; + + /** 已使用缓存空间 */ + protected int usedSize; + + /** + * 构造 + * @param capacity 缓存容量 + * @param maxFileSize 文件最大大小 + * @param timeout 默认超时时间,0表示无默认超时 + */ + public AbstractFileCache(int capacity, int maxFileSize, long timeout) { + this.capacity = capacity; + this.maxFileSize = maxFileSize; + this.timeout = timeout; + this.cache = initCache(); + } + + /** + * @return 缓存容量(byte数) + */ + public int capacity() { + return capacity; + } + + /** + * @return 已使用空间大小(byte数) + */ + public int getUsedSize() { + return usedSize; + } + + /** + * @return 允许被缓存文件的最大byte数 + */ + public int maxFileSize() { + return maxFileSize; + } + + /** + * @return 缓存的文件数 + */ + public int getCachedFilesCount() { + return cache.size(); + } + + /** + * @return 超时时间 + */ + public long timeout() { + return this.timeout; + } + + /** + * 清空缓存 + */ + public void clear() { + cache.clear(); + usedSize = 0; + } + + // ---------------------------------------------------------------- get + + /** + * 获得缓存过的文件bytes + * @param path 文件路径 + * @return 缓存过的文件bytes + * @throws IORuntimeException IO异常 + */ + public byte[] getFileBytes(String path) throws IORuntimeException { + return getFileBytes(new File(path)); + } + + /** + * 获得缓存过的文件bytes + * @param file 文件 + * @return 缓存过的文件bytes + * @throws IORuntimeException IO异常 + */ + public byte[] getFileBytes(File file) throws IORuntimeException { + byte[] bytes = cache.get(file); + if (bytes != null) { + return bytes; + } + + // add file + bytes = FileUtil.readBytes(file); + + if ((maxFileSize != 0) && (file.length() > maxFileSize)) { + //大于缓存空间,不缓存,直接返回 + return bytes; + } + + usedSize += bytes.length; + + //文件放入缓存,如果usedSize > capacity,purge()方法将被调用 + cache.put(file, bytes); + + return bytes; + } + + // ---------------------------------------------------------------- protected method start + /** + * 初始化实现文件缓存的缓存对象 + * @return {@link Cache} + */ + protected abstract Cache initCache(); + // ---------------------------------------------------------------- protected method end + +} diff --git a/hutool-cache/src/main/java/cn/hutool/cache/file/LFUFileCache.java b/hutool-cache/src/main/java/cn/hutool/cache/file/LFUFileCache.java new file mode 100644 index 000000000..5bb225845 --- /dev/null +++ b/hutool-cache/src/main/java/cn/hutool/cache/file/LFUFileCache.java @@ -0,0 +1,64 @@ +package cn.hutool.cache.file; + +import java.io.File; + +import cn.hutool.cache.Cache; +import cn.hutool.cache.impl.LFUCache; + +/** + * 使用LFU缓存文件,以解决频繁读取文件引起的性能问题 + * @author Looly + * + */ +public class LFUFileCache extends AbstractFileCache{ + private static final long serialVersionUID = 1L; + + /** + * 构造
+ * 最大文件大小为缓存容量的一半
+ * 默认无超时 + * @param capacity 缓存容量 + */ + public LFUFileCache(int capacity) { + this(capacity, capacity / 2, 0); + } + + /** + * 构造
+ * 默认无超时 + * @param capacity 缓存容量 + * @param maxFileSize 最大文件大小 + */ + public LFUFileCache(int capacity, int maxFileSize) { + this(capacity, maxFileSize, 0); + } + + /** + * 构造 + * @param capacity 缓存容量 + * @param maxFileSize 文件最大大小 + * @param timeout 默认超时时间,0表示无默认超时 + */ + public LFUFileCache(int capacity, int maxFileSize, long timeout) { + super(capacity, maxFileSize, timeout); + } + + @Override + protected Cache initCache() { + Cache cache = new LFUCache(this.capacity, this.timeout) { + private static final long serialVersionUID = 1L; + + @Override + public boolean isFull() { + return LFUFileCache.this.usedSize > this.capacity; + } + + @Override + protected void onRemove(File key, byte[] cachedObject) { + usedSize -= cachedObject.length; + } + }; + return cache; + } + +} diff --git a/hutool-cache/src/main/java/cn/hutool/cache/file/LRUFileCache.java b/hutool-cache/src/main/java/cn/hutool/cache/file/LRUFileCache.java new file mode 100644 index 000000000..f8ca3ecf2 --- /dev/null +++ b/hutool-cache/src/main/java/cn/hutool/cache/file/LRUFileCache.java @@ -0,0 +1,64 @@ +package cn.hutool.cache.file; + +import java.io.File; + +import cn.hutool.cache.Cache; +import cn.hutool.cache.impl.LRUCache; + +/** + * 使用LRU缓存文件,以解决频繁读取文件引起的性能问题 + * @author Looly + * + */ +public class LRUFileCache extends AbstractFileCache{ + private static final long serialVersionUID = 1L; + + /** + * 构造
+ * 最大文件大小为缓存容量的一半
+ * 默认无超时 + * @param capacity 缓存容量 + */ + public LRUFileCache(int capacity) { + this(capacity, capacity / 2, 0); + } + + /** + * 构造
+ * 默认无超时 + * @param capacity 缓存容量 + * @param maxFileSize 最大文件大小 + */ + public LRUFileCache(int capacity, int maxFileSize) { + this(capacity, maxFileSize, 0); + } + + /** + * 构造 + * @param capacity 缓存容量 + * @param maxFileSize 文件最大大小 + * @param timeout 默认超时时间,0表示无默认超时 + */ + public LRUFileCache(int capacity, int maxFileSize, long timeout) { + super(capacity, maxFileSize, timeout); + } + + @Override + protected Cache initCache() { + Cache cache = new LRUCache(this.capacity, super.timeout) { + private static final long serialVersionUID = 1L; + + @Override + public boolean isFull() { + return LRUFileCache.this.usedSize > this.capacity; + } + + @Override + protected void onRemove(File key, byte[] cachedObject) { + usedSize -= cachedObject.length; + } + }; + return cache; + } + +} diff --git a/hutool-cache/src/main/java/cn/hutool/cache/file/package-info.java b/hutool-cache/src/main/java/cn/hutool/cache/file/package-info.java new file mode 100644 index 000000000..9a037f8fa --- /dev/null +++ b/hutool-cache/src/main/java/cn/hutool/cache/file/package-info.java @@ -0,0 +1,7 @@ +/** + * 提供针对文件的缓存实现 + * + * @author looly + * + */ +package cn.hutool.cache.file; \ No newline at end of file diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java new file mode 100644 index 000000000..da0fa2e82 --- /dev/null +++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java @@ -0,0 +1,359 @@ +package cn.hutool.cache.impl; + +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; +import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; + +import cn.hutool.cache.Cache; +import cn.hutool.core.collection.CopiedIter; +import cn.hutool.core.lang.func.Func0; + +/** + * 超时和限制大小的缓存的默认实现
+ * 继承此抽象缓存需要:
+ *

    + *
  • 创建一个新的Map
  • + *
  • 实现 prune 策略
  • + *
+ * + * @author Looly,jodd + * + * @param 键类型 + * @param 值类型 + */ +public abstract class AbstractCache implements Cache { + private static final long serialVersionUID = 1L; + + protected Map> cacheMap; + + private final ReentrantReadWriteLock cacheLock = new ReentrantReadWriteLock(); + private final ReadLock readLock = cacheLock.readLock(); + private final WriteLock writeLock = cacheLock.writeLock(); + + /** 返回缓存容量,0表示无大小限制 */ + protected int capacity; + /** 缓存失效时长, 0 表示无限制,单位毫秒 */ + protected long timeout; + + /** 每个对象是否有单独的失效时长,用于决定清理过期对象是否有必要。 */ + protected boolean existCustomTimeout; + + /** 命中数 */ + protected int hitCount; + /** 丢失数 */ + protected int missCount; + + // ---------------------------------------------------------------- put start + @Override + public void put(K key, V object) { + put(key, object, timeout); + } + + @Override + public void put(K key, V object, long timeout) { + writeLock.lock(); + + try { + putWithoutLock(key, object, timeout); + } finally { + writeLock.unlock(); + } + } + + /** + * 加入元素,无锁 + * + * @param key 键 + * @param object 值 + * @param timeout 超时时长 + * @since 4.5.16 + */ + private void putWithoutLock(K key, V object, long timeout) { + CacheObj co = new CacheObj(key, object, timeout); + if (timeout != 0) { + existCustomTimeout = true; + } + if (isFull()) { + pruneCache(); + } + cacheMap.put(key, co); + } + // ---------------------------------------------------------------- put end + + // ---------------------------------------------------------------- get start + @Override + public boolean containsKey(K key) { + readLock.lock(); + + try { + // 不存在或已移除 + final CacheObj co = cacheMap.get(key); + if (co == null) { + return false; + } + + if (false == co.isExpired()) { + // 命中 + return true; + } + } finally { + readLock.unlock(); + } + + // 过期 + remove(key, true); + return false; + } + + /** + * @return 命中数 + */ + public int getHitCount() { + this.readLock.lock(); + try { + return hitCount; + } finally { + this.readLock.unlock(); + } + } + + /** + * @return 丢失数 + */ + public int getMissCount() { + this.readLock.lock(); + try { + return missCount; + } finally { + this.readLock.unlock(); + } + } + + @Override + public V get(K key) { + return get(key, true); + } + + @Override + public V get(K key, Func0 supplier) { + V v = get(key); + if (null == v && null != supplier) { + writeLock.lock(); + try { + // 双重检查锁 + final CacheObj co = cacheMap.get(key); + if(null == co || co.isExpired() || null == co.getValue()) { + try { + v = supplier.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + putWithoutLock(key, v, this.timeout); + } else { + v = co.get(true); + } + } finally { + writeLock.unlock(); + } + } + return v; + } + + @Override + public V get(K key, boolean isUpdateLastAccess) { + readLock.lock(); + + try { + // 不存在或已移除 + final CacheObj co = cacheMap.get(key); + if (co == null) { + missCount++; + return null; + } + + if (false == co.isExpired()) { + // 命中 + hitCount++; + return co.get(isUpdateLastAccess); + } + } finally { + readLock.unlock(); + } + + // 过期 + remove(key, true); + return null; + } + + // ---------------------------------------------------------------- get end + + @Override + @SuppressWarnings("unchecked") + public Iterator iterator() { + CacheObjIterator copiedIterator = (CacheObjIterator) this.cacheObjIterator(); + return new CacheValuesIterator(copiedIterator); + } + + @Override + public Iterator> cacheObjIterator() { + CopiedIter> copiedIterator; + readLock.lock(); + try { + copiedIterator = CopiedIter.copyOf(this.cacheMap.values().iterator()); + } finally { + readLock.unlock(); + } + return new CacheObjIterator<>(copiedIterator); + } + + // ---------------------------------------------------------------- prune start + /** + * 清理实现 + * + * @return 清理数 + */ + protected abstract int pruneCache(); + + @Override + public final int prune() { + writeLock.lock(); + try { + return pruneCache(); + } finally { + writeLock.unlock(); + } + } + // ---------------------------------------------------------------- prune end + + // ---------------------------------------------------------------- common start + @Override + public int capacity() { + return capacity; + } + + /** + * @return 默认缓存失效时长。
+ * 每个对象可以单独设置失效时长 + */ + @Override + public long timeout() { + return timeout; + } + + /** + * 只有设置公共缓存失效时长或每个对象单独的失效时长时清理可用 + * + * @return 过期对象清理是否可用,内部使用 + */ + protected boolean isPruneExpiredActive() { + this.readLock.lock(); + try { + return (timeout != 0) || existCustomTimeout; + } finally { + this.readLock.unlock(); + } + } + + @Override + public boolean isFull() { + this.readLock.lock(); + try { + return (capacity > 0) && (cacheMap.size() >= capacity); + } finally { + this.readLock.unlock(); + } + } + + @Override + public void remove(K key) { + remove(key, false); + } + + @Override + public void clear() { + writeLock.lock(); + try { + cacheMap.clear(); + } finally { + writeLock.unlock(); + } + } + + @Override + public int size() { + this.readLock.lock(); + try { + return cacheMap.size(); + } finally { + this.readLock.unlock(); + } + } + + @Override + public boolean isEmpty() { + this.readLock.lock(); + try { + return cacheMap.isEmpty(); + } finally { + this.readLock.unlock(); + } + } + + @Override + public String toString() { + this.readLock.lock(); + try { + return this.cacheMap.toString(); + } finally { + this.readLock.unlock(); + } + } + // ---------------------------------------------------------------- common end + + /** + * 对象移除回调。默认无动作 + * + * @param key 键 + * @param cachedObject 被缓存的对象 + */ + protected void onRemove(K key, V cachedObject) { + // ignore + } + + /** + * 移除key对应的对象 + * + * @param key 键 + * @param withMissCount 是否计数丢失数 + */ + private void remove(K key, boolean withMissCount) { + writeLock.lock(); + CacheObj co; + try { + co = removeWithoutLock(key, withMissCount); + } finally { + writeLock.unlock(); + } + if (null != co) { + onRemove(co.key, co.obj); + } + } + + /** + * 移除key对应的对象,不加锁 + * + * @param key 键 + * @param withMissCount 是否计数丢失数 + * @return 移除的对象,无返回null + */ + private CacheObj removeWithoutLock(K key, boolean withMissCount) { + final CacheObj co = cacheMap.remove(key); + if (withMissCount) { + // 在丢失计数有效的情况下,移除一般为get时的超时操作,此处应该丢失数+1 + this.missCount++; + } + return co; + } +} diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/CacheObj.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/CacheObj.java new file mode 100644 index 000000000..0447c27f9 --- /dev/null +++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/CacheObj.java @@ -0,0 +1,92 @@ +package cn.hutool.cache.impl; + +import java.io.Serializable; + +/** + * 缓存对象 + * @author Looly + * + * @param Key类型 + * @param Value类型 + */ +public class CacheObj implements Serializable{ + private static final long serialVersionUID = 1L; + + protected final K key; + protected final V obj; + + /** 上次访问时间 */ + private long lastAccess; + /** 访问次数 */ + protected long accessCount; + /** 对象存活时长,0表示永久存活*/ + private long ttl; + + /** + * 构造 + * + * @param key 键 + * @param obj 值 + * @param ttl 超时时长 + */ + protected CacheObj(K key, V obj, long ttl) { + this.key = key; + this.obj = obj; + this.ttl = ttl; + this.lastAccess = System.currentTimeMillis(); + } + + /** + * 判断是否过期 + * + * @return 是否过期 + */ + boolean isExpired() { + if(this.ttl > 0) { + final long expiredTime = this.lastAccess + this.ttl; + if(expiredTime > 0 && expiredTime < System.currentTimeMillis()) { + // expiredTime > 0 杜绝Long类型溢出变负数问题,当当前时间超过过期时间,表示过期 + return true; + } + } + return false; + } + + /** + * 获取值 + * + * @param isUpdateLastAccess 是否更新最后访问时间 + * @return 获得对象 + * @since 4.0.10 + */ + V get(boolean isUpdateLastAccess) { + if(isUpdateLastAccess) { + lastAccess = System.currentTimeMillis(); + } + accessCount++; + return this.obj; + } + + /** + * 获取键 + * @return 键 + * @since 4.0.10 + */ + public K getKey() { + return this.key; + } + + /** + * 获取值 + * @return 值 + * @since 4.0.10 + */ + public V getValue() { + return this.obj; + } + + @Override + public String toString() { + return "CacheObj [key=" + key + ", obj=" + obj + ", lastAccess=" + lastAccess + ", accessCount=" + accessCount + ", ttl=" + ttl + "]"; + } +} diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/CacheObjIterator.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/CacheObjIterator.java new file mode 100644 index 000000000..e7f1f5759 --- /dev/null +++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/CacheObjIterator.java @@ -0,0 +1,74 @@ +package cn.hutool.cache.impl; + +import java.io.Serializable; +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * {@link cn.hutool.cache.impl.AbstractCache} 的CacheObj迭代器. + * + * @author looly + * + * @param 键类型 + * @param 值类型 + * @since 4.0.10 + */ +public class CacheObjIterator implements Iterator>, Serializable { + private static final long serialVersionUID = 1L; + + private final Iterator> iterator; + private CacheObj nextValue; + + /** + * 构造 + * + * @param iterator 原{@link Iterator} + * @param readLock 读锁 + */ + CacheObjIterator(Iterator> iterator) { + this.iterator = iterator; + nextValue(); + } + + /** + * @return 是否有下一个值 + */ + @Override + public boolean hasNext() { + return nextValue != null; + } + + /** + * @return 下一个值 + */ + @Override + public CacheObj next() { + if (false == hasNext()) { + throw new NoSuchElementException(); + } + final CacheObj cachedObject = nextValue; + nextValue(); + return cachedObject; + } + + /** + * 从缓存中移除没有过期的当前值,此方法不支持 + */ + @Override + public void remove() { + throw new UnsupportedOperationException("Cache values Iterator is not support to modify."); + } + + /** + * 下一个值,当不存在则下一个值为null + */ + private void nextValue() { + while (iterator.hasNext()) { + nextValue = iterator.next(); + if (nextValue.isExpired() == false) { + return; + } + } + nextValue = null; + } +} diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/CacheValuesIterator.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/CacheValuesIterator.java new file mode 100644 index 000000000..fcbbec048 --- /dev/null +++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/CacheValuesIterator.java @@ -0,0 +1,49 @@ +package cn.hutool.cache.impl; + +import java.io.Serializable; +import java.util.Iterator; + +/** + * {@link cn.hutool.cache.impl.AbstractCache} 的值迭代器. + * @author looly + * + * @param 迭代对象类型 + */ +public class CacheValuesIterator implements Iterator, Serializable { + private static final long serialVersionUID = 1L; + + private final CacheObjIterator cacheObjIter; + + /** + * 构造 + * @param iterator 原{@link CacheObjIterator} + * @param readLock 读锁 + */ + CacheValuesIterator(CacheObjIterator iterator) { + this.cacheObjIter = iterator; + } + + /** + * @return 是否有下一个值 + */ + @Override + public boolean hasNext() { + return this.cacheObjIter.hasNext(); + } + + /** + * @return 下一个值 + */ + @Override + public V next() { + return cacheObjIter.next().getValue(); + } + + /** + * 从缓存中移除没有过期的当前值,不支持此方法 + */ + @Override + public void remove() { + cacheObjIter.remove(); + } +} diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/FIFOCache.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/FIFOCache.java new file mode 100644 index 000000000..f61f740d7 --- /dev/null +++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/FIFOCache.java @@ -0,0 +1,78 @@ +package cn.hutool.cache.impl; + +import java.util.Iterator; +import java.util.LinkedHashMap; + +/** + * FIFO(first in first out) 先进先出缓存. + * + *

+ * 元素不停的加入缓存直到缓存满为止,当缓存满时,清理过期缓存对象,清理后依旧满则删除先入的缓存(链表首部对象)
+ * 优点:简单快速
+ * 缺点:不灵活,不能保证最常用的对象总是被保留 + *

+ * + * @author Looly + * + * @param 键类型 + * @param 值类型 + */ +public class FIFOCache extends AbstractCache { + private static final long serialVersionUID = 1L; + + /** + * 构造,默认对象不过期 + * + * @param capacity 容量 + */ + public FIFOCache(int capacity) { + this(capacity, 0); + } + + /** + * 构造 + * + * @param capacity 容量 + * @param timeout 过期时长 + */ + public FIFOCache(int capacity, long timeout) { + if(Integer.MAX_VALUE == capacity) { + capacity -= 1; + } + + this.capacity = capacity; + this.timeout = timeout; + cacheMap = new LinkedHashMap>(capacity + 1, 1.0f, false); + } + + /** + * 先进先出的清理策略
+ * 先遍历缓存清理过期的缓存对象,如果清理后还是满的,则删除第一个缓存对象 + */ + @Override + protected int pruneCache() { + int count = 0; + CacheObj first = null; + + // 清理过期对象并找出链表头部元素(先入元素) + Iterator> values = cacheMap.values().iterator(); + while (values.hasNext()) { + CacheObj co = values.next(); + if (co.isExpired()) { + values.remove(); + count++; + } + if (first == null) { + first = co; + } + } + + // 清理结束后依旧是满的,则删除第一个被缓存的对象 + if (isFull() && null != first) { + cacheMap.remove(first.key); + onRemove(first.key, first.obj); + count++; + } + return count; + } +} diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/LFUCache.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/LFUCache.java new file mode 100644 index 000000000..862c9d8a7 --- /dev/null +++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/LFUCache.java @@ -0,0 +1,96 @@ +package cn.hutool.cache.impl; + +import java.util.HashMap; +import java.util.Iterator; + +/** + * LFU(least frequently used) 最少使用率缓存
+ * 根据使用次数来判定对象是否被持续缓存
+ * 使用率是通过访问次数计算的。
+ * 当缓存满时清理过期对象。
+ * 清理后依旧满的情况下清除最少访问(访问计数最小)的对象并将其他对象的访问数减去这个最小访问数,以便新对象进入后可以公平计数。 + * + * @author Looly,jodd + * + * @param 键类型 + * @param 值类型 + */ +public class LFUCache extends AbstractCache { + private static final long serialVersionUID = 1L; + + /** + * 构造 + * + * @param capacity 容量 + */ + public LFUCache(int capacity) { + this(capacity, 0); + } + + /** + * 构造 + * + * @param capacity 容量 + * @param timeout 过期时长 + */ + public LFUCache(int capacity, long timeout) { + if(Integer.MAX_VALUE == capacity) { + capacity -= 1; + } + + this.capacity = capacity; + this.timeout = timeout; + cacheMap = new HashMap>(capacity + 1, 1.0f); + } + + // ---------------------------------------------------------------- prune + + /** + * 清理过期对象。
+ * 清理后依旧满的情况下清除最少访问(访问计数最小)的对象并将其他对象的访问数减去这个最小访问数,以便新对象进入后可以公平计数。 + * + * @return 清理个数 + */ + @Override + protected int pruneCache() { + int count = 0; + CacheObj comin = null; + + // 清理过期对象并找出访问最少的对象 + Iterator> values = cacheMap.values().iterator(); + CacheObj co; + while (values.hasNext()) { + co = values.next(); + if (co.isExpired() == true) { + values.remove(); + onRemove(co.key, co.obj); + count++; + continue; + } + + //找出访问最少的对象 + if (comin == null || co.accessCount < comin.accessCount) { + comin = co; + } + } + + // 减少所有对象访问量,并清除减少后为0的访问对象 + if (isFull() && comin != null) { + long minAccessCount = comin.accessCount; + + values = cacheMap.values().iterator(); + CacheObj co1; + while (values.hasNext()) { + co1 = values.next(); + co1.accessCount -= minAccessCount; + if (co1.accessCount <= 0) { + values.remove(); + onRemove(co1.key, co1.obj); + count++; + } + } + } + + return count; + } +} diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/LRUCache.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/LRUCache.java new file mode 100644 index 000000000..dcf0fe785 --- /dev/null +++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/LRUCache.java @@ -0,0 +1,71 @@ +package cn.hutool.cache.impl; + +import java.util.Iterator; + +import cn.hutool.core.map.FixedLinkedHashMap; + +/** + * LRU (least recently used)最近最久未使用缓存
+ * 根据使用时间来判定对象是否被持续缓存
+ * 当对象被访问时放入缓存,当缓存满了,最久未被使用的对象将被移除。
+ * 此缓存基于LinkedHashMap,因此当被缓存的对象每被访问一次,这个对象的key就到链表头部。
+ * 这个算法简单并且非常快,他比FIFO有一个显著优势是经常使用的对象不太可能被移除缓存。
+ * 缺点是当缓存满时,不能被很快的访问。 + * @author Looly,jodd + * + * @param 键类型 + * @param 值类型 + */ +public class LRUCache extends AbstractCache { + private static final long serialVersionUID = 1L; + + /** + * 构造
+ * 默认无超时 + * @param capacity 容量 + */ + public LRUCache(int capacity) { + this(capacity, 0); + } + + /** + * 构造 + * @param capacity 容量 + * @param timeout 默认超时时间,单位:毫秒 + */ + public LRUCache(int capacity, long timeout) { + if(Integer.MAX_VALUE == capacity) { + capacity -= 1; + } + + this.capacity = capacity; + this.timeout = timeout; + + //链表key按照访问顺序排序,调用get方法后,会将这次访问的元素移至头部 + cacheMap = new FixedLinkedHashMap>(capacity); + } + + // ---------------------------------------------------------------- prune + + /** + * 只清理超时对象,LRU的实现会交给LinkedHashMap + */ + @Override + protected int pruneCache() { + if (isPruneExpiredActive() == false) { + return 0; + } + int count = 0; + Iterator> values = cacheMap.values().iterator(); + CacheObj co; + while (values.hasNext()) { + co = values.next(); + if (co.isExpired()) { + values.remove(); + onRemove(co.key, co.obj); + count++; + } + } + return count; + } +} diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/NoCache.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/NoCache.java new file mode 100644 index 000000000..0964109cd --- /dev/null +++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/NoCache.java @@ -0,0 +1,102 @@ +package cn.hutool.cache.impl; + +import java.util.Iterator; + +import cn.hutool.cache.Cache; +import cn.hutool.core.lang.func.Func0; + +/** + * 无缓存实现,用于快速关闭缓存 + * + * @param 键类型 + * @param 值类型 + * @author Looly,jodd + */ +public class NoCache implements Cache { + private static final long serialVersionUID = 1L; + + @Override + public int capacity() { + return 0; + } + + @Override + public long timeout() { + return 0; + } + + @Override + public void put(K key, V object) { + // 跳过 + } + + @Override + public void put(K key, V object, long timeout) { + // 跳过 + } + + @Override + public boolean containsKey(K key) { + return false; + } + + @Override + public V get(K key) { + return null; + } + + @Override + public V get(K key, boolean isUpdateLastAccess) { + return null; + } + + @Override + public V get(K key, Func0 supplier) { + try { + return (null == supplier) ? null : supplier.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public Iterator iterator() { + return null; + } + + @Override + public Iterator> cacheObjIterator() { + return null; + } + + @Override + public int prune() { + return 0; + } + + @Override + public boolean isFull() { + return false; + } + + @Override + public void remove(K key) { + // 跳过 + } + + @Override + public void clear() { + // 跳过 + } + + @Override + public int size() { + return 0; + } + + @Override + public boolean isEmpty() { + return false; + } + +} diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/TimedCache.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/TimedCache.java new file mode 100644 index 000000000..a5677083f --- /dev/null +++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/TimedCache.java @@ -0,0 +1,92 @@ +package cn.hutool.cache.impl; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ScheduledFuture; + +import cn.hutool.cache.GlobalPruneTimer; + +/** + * 定时缓存
+ * 此缓存没有容量限制,对象只有在过期后才会被移除 + * + * @author Looly + * + * @param 键类型 + * @param 值类型 + */ +public class TimedCache extends AbstractCache { + private static final long serialVersionUID = 1L; + + /** 正在执行的定时任务 */ + private ScheduledFuture pruneJobFuture; + + /** + * 构造 + * + * @param timeout 超时(过期)时长,单位毫秒 + */ + public TimedCache(long timeout) { + this(timeout, new HashMap>()); + } + + /** + * 构造 + * + * @param timeout 过期时长 + * @param map 存储缓存对象的map + */ + public TimedCache(long timeout, Map> map) { + this.capacity = 0; + this.timeout = timeout; + this.cacheMap = map; + } + + // ---------------------------------------------------------------- prune + /** + * 清理过期对象 + * + * @return 清理数 + */ + @Override + protected int pruneCache() { + int count = 0; + Iterator> values = cacheMap.values().iterator(); + CacheObj co; + while (values.hasNext()) { + co = values.next(); + if (co.isExpired()) { + values.remove(); + onRemove(co.key, co.obj); + count++; + } + } + return count; + } + + // ---------------------------------------------------------------- auto prune + /** + * 定时清理 + * + * @param delay 间隔时长,单位毫秒 + */ + public void schedulePrune(long delay) { + this.pruneJobFuture = GlobalPruneTimer.INSTANCE.schedule(new Runnable() { + @Override + public void run() { + prune(); + } + }, delay); + } + + /** + * 取消定时清理 + */ + public void cancelPruneSchedule() { + if (null != pruneJobFuture) { + pruneJobFuture.cancel(true); + } + } + +} diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/WeakCache.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/WeakCache.java new file mode 100644 index 000000000..2fe027ce0 --- /dev/null +++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/WeakCache.java @@ -0,0 +1,24 @@ +package cn.hutool.cache.impl; + +import java.util.WeakHashMap; + +/** + * 弱引用缓存
+ * 对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃,这就使该键成为可终止的,被终止,然后被回收。
+ * 丢弃某个键时,其条目从映射中有效地移除。
+ * + * @author Looly + * + * @param 键 + * @param 值 + * @author looly + * @since 3.0.7 + */ +public class WeakCache extends TimedCache{ + private static final long serialVersionUID = 1L; + + public WeakCache(long timeout) { + super(timeout, new WeakHashMap>()); + } + +} diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/package-info.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/package-info.java new file mode 100644 index 000000000..b85c8fb40 --- /dev/null +++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/package-info.java @@ -0,0 +1,7 @@ +/** + * 提供各种缓存实现 + * + * @author looly + * + */ +package cn.hutool.cache.impl; \ No newline at end of file diff --git a/hutool-cache/src/main/java/cn/hutool/cache/package-info.java b/hutool-cache/src/main/java/cn/hutool/cache/package-info.java new file mode 100644 index 000000000..9c5ac3a6f --- /dev/null +++ b/hutool-cache/src/main/java/cn/hutool/cache/package-info.java @@ -0,0 +1,7 @@ +/** + * 提供简易的缓存实现,此模块参考了jodd工具中的Cache模块 + * + * @author looly + * + */ +package cn.hutool.cache; \ No newline at end of file diff --git a/hutool-cache/src/test/java/cn/hutool/cache/test/CacheConcurrentTest.java b/hutool-cache/src/test/java/cn/hutool/cache/test/CacheConcurrentTest.java new file mode 100644 index 000000000..166c57a00 --- /dev/null +++ b/hutool-cache/src/test/java/cn/hutool/cache/test/CacheConcurrentTest.java @@ -0,0 +1,101 @@ +package cn.hutool.cache.test; + +import java.util.Iterator; + +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.cache.Cache; +import cn.hutool.cache.impl.FIFOCache; +import cn.hutool.cache.impl.LRUCache; +import cn.hutool.core.lang.Console; +import cn.hutool.core.thread.ThreadUtil; + +/** + * 缓存单元测试 + * + * @author looly + * + */ +public class CacheConcurrentTest { + + @Test + @Ignore + public void fifoCacheTest() { + int threadCount = 4000; + final Cache cache = new FIFOCache<>(3); + + // 由于缓存容量只有3,当加入第四个元素的时候,根据FIFO规则,最先放入的对象将被移除 + + for (int i = 0; i < threadCount; i++) { + ThreadUtil.execute(new Runnable() { + @Override + public void run() { + cache.put("key1", "value1", System.currentTimeMillis() * 3); + cache.put("key2", "value2", System.currentTimeMillis() * 3); + cache.put("key3", "value3", System.currentTimeMillis() * 3); + cache.put("key4", "value4", System.currentTimeMillis() * 3); + ThreadUtil.sleep(1000); + cache.put("key5", "value5", System.currentTimeMillis() * 3); + cache.put("key6", "value6", System.currentTimeMillis() * 3); + cache.put("key7", "value7", System.currentTimeMillis() * 3); + cache.put("key8", "value8", System.currentTimeMillis() * 3); + Console.log("put all"); + } + }); + } + + for (int i = 0; i < threadCount; i++) { + ThreadUtil.execute(new Runnable() { + @Override + public void run() { + show(cache); + } + }); + } + + System.out.println("=============================="); + ThreadUtil.sleep(10000); + } + + @Test + @Ignore + public void lruCacheTest() { + int threadCount = 40000; + final Cache cache = new LRUCache<>(1000); + + for (int i = 0; i < threadCount; i++) { + final int index = i; + ThreadUtil.execute(new Runnable() { + @Override + public void run() { + cache.put("key1"+ index, "value1"); + cache.put("key2"+ index, "value2", System.currentTimeMillis() * 3); + + int size = cache.size(); + int capacity = cache.capacity(); + if(size > capacity) { + Console.log("{} {}", size, capacity); + } + ThreadUtil.sleep(1000); + size = cache.size(); + capacity = cache.capacity(); + if(size > capacity) { + Console.log("## {} {}", size, capacity); + } + } + }); + } + + ThreadUtil.sleep(5000); + } + + private void show(Cache cache) { + Iterator its = cache.iterator(); + + while (its.hasNext()) { + Object tt = its.next(); + Console.log(tt); + } + } +} diff --git a/hutool-cache/src/test/java/cn/hutool/cache/test/CacheTest.java b/hutool-cache/src/test/java/cn/hutool/cache/test/CacheTest.java new file mode 100644 index 000000000..c8720acd5 --- /dev/null +++ b/hutool-cache/src/test/java/cn/hutool/cache/test/CacheTest.java @@ -0,0 +1,111 @@ +package cn.hutool.cache.test; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.cache.Cache; +import cn.hutool.cache.CacheUtil; +import cn.hutool.cache.impl.TimedCache; +import cn.hutool.core.date.DateUnit; +import cn.hutool.core.lang.func.Func0; +import cn.hutool.core.thread.ThreadUtil; + +/** + * 缓存测试用例 + * @author Looly + * + */ +public class CacheTest { + + @Test + public void fifoCacheTest(){ + Cache fifoCache = CacheUtil.newFIFOCache(3); + fifoCache.put("key1", "value1", DateUnit.SECOND.getMillis() * 3); + fifoCache.put("key2", "value2", DateUnit.SECOND.getMillis() * 3); + fifoCache.put("key3", "value3", DateUnit.SECOND.getMillis() * 3); + fifoCache.put("key4", "value4", DateUnit.SECOND.getMillis() * 3); + + //由于缓存容量只有3,当加入第四个元素的时候,根据FIFO规则,最先放入的对象将被移除 + String value1 = fifoCache.get("key1"); + Assert.assertTrue(null == value1); + } + + @Test + public void lfuCacheTest(){ + Cache lfuCache = CacheUtil.newLFUCache(3); + lfuCache.put("key1", "value1", DateUnit.SECOND.getMillis() * 3); + //使用次数+1 + lfuCache.get("key1"); + lfuCache.put("key2", "value2", DateUnit.SECOND.getMillis() * 3); + lfuCache.put("key3", "value3", DateUnit.SECOND.getMillis() * 3); + lfuCache.put("key4", "value4", DateUnit.SECOND.getMillis() * 3); + + //由于缓存容量只有3,当加入第四个元素的时候,根据LFU规则,最少使用的将被移除(2,3被移除) + String value1 = lfuCache.get("key1"); + String value2 = lfuCache.get("key2"); + String value3 = lfuCache.get("key3"); + Assert.assertTrue(null != value1); + Assert.assertTrue(null == value2); + Assert.assertTrue(null == value3); + } + + @Test + public void lruCacheTest(){ + Cache lruCache = CacheUtil.newLRUCache(3); + //通过实例化对象创建 +// LRUCache lruCache = new LRUCache(3); + lruCache.put("key1", "value1", DateUnit.SECOND.getMillis() * 3); + lruCache.put("key2", "value2", DateUnit.SECOND.getMillis() * 3); + lruCache.put("key3", "value3", DateUnit.SECOND.getMillis() * 3); + //使用时间推近 + lruCache.get("key1"); + lruCache.put("key4", "value4", DateUnit.SECOND.getMillis() * 3); + + String value1 = lruCache.get("key1"); + Assert.assertNotNull(value1); + //由于缓存容量只有3,当加入第四个元素的时候,根据LRU规则,最少使用的将被移除(2被移除) + String value2 = lruCache.get("key2"); + Assert.assertNull(value2); + } + + @Test + public void timedCacheTest(){ + TimedCache timedCache = CacheUtil.newTimedCache(4); +// TimedCache timedCache = new TimedCache(DateUnit.SECOND.getMillis() * 3); + timedCache.put("key1", "value1", 1);//1毫秒过期 + timedCache.put("key2", "value2", DateUnit.SECOND.getMillis() * 5);//5秒过期 + timedCache.put("key3", "value3");//默认过期(4毫秒) + timedCache.put("key4", "value4", Long.MAX_VALUE);//永不过期 + + //启动定时任务,每5毫秒秒检查一次过期 + timedCache.schedulePrune(5); + //等待5毫秒 + ThreadUtil.sleep(5); + + //5毫秒后由于value2设置了5毫秒过期,因此只有value2被保留下来 + String value1 = timedCache.get("key1"); + Assert.assertTrue(null == value1); + String value2 = timedCache.get("key2"); + Assert.assertEquals("value2", value2); + + //5毫秒后,由于设置了默认过期,key3只被保留4毫秒,因此为null + String value3 = timedCache.get("key3"); + Assert.assertTrue(null == value3); + + String value3Supplier = timedCache.get("key3", new Func0() { + + @Override + public String call() throws Exception { + return "Default supplier"; + } + }); + Assert.assertEquals("Default supplier", value3Supplier); + + // 永不过期 + String value4 = timedCache.get("key4"); + Assert.assertEquals("value4", value4); + + //取消定时清理 + timedCache.cancelPruneSchedule(); + } +} diff --git a/hutool-cache/src/test/java/cn/hutool/cache/test/FileCacheTest.java b/hutool-cache/src/test/java/cn/hutool/cache/test/FileCacheTest.java new file mode 100644 index 000000000..359774866 --- /dev/null +++ b/hutool-cache/src/test/java/cn/hutool/cache/test/FileCacheTest.java @@ -0,0 +1,19 @@ +package cn.hutool.cache.test; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.cache.file.LFUFileCache; + +/** + * 文件缓存单元测试 + * @author looly + * + */ +public class FileCacheTest { + @Test + public void lfuFileCacheTest() { + LFUFileCache cache = new LFUFileCache(1000, 500, 2000); + Assert.assertNotNull(cache); + } +} diff --git a/hutool-captcha/pom.xml b/hutool-captcha/pom.xml new file mode 100644 index 000000000..f53d79da7 --- /dev/null +++ b/hutool-captcha/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + jar + + + cn.hutool + hutool-parent + 4.6.2-SNAPSHOT + + + hutool-captcha + ${project.artifactId} + Hutool 验证码工具 + + + + cn.hutool + hutool-core + ${project.parent.version} + + + diff --git a/hutool-captcha/src/main/java/cn/hutool/captcha/AbstractCaptcha.java b/hutool-captcha/src/main/java/cn/hutool/captcha/AbstractCaptcha.java new file mode 100644 index 000000000..482c1a531 --- /dev/null +++ b/hutool-captcha/src/main/java/cn/hutool/captcha/AbstractCaptcha.java @@ -0,0 +1,226 @@ +package cn.hutool.captcha; + +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Font; +import java.awt.Image; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.RandomGenerator; +import cn.hutool.core.codec.Base64; +import cn.hutool.core.img.ImgUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; + +/** + * 抽象验证码
+ * 抽象验证码实现了验证码字符串的生成、验证,验证码图片的写出
+ * 实现类通过实现{@link #createImage(String)} 方法生成图片对象 + * + * @author looly + * + */ +public abstract class AbstractCaptcha implements ICaptcha { + private static final long serialVersionUID = 3180820918087507254L; + + /** 图片的宽度 */ + protected int width = 100; + /** 图片的高度 */ + protected int height = 37; + /** 验证码干扰元素个数 */ + protected int interfereCount = 15; + /** 字体 */ + protected Font font; + /** 验证码 */ + protected String code; + /** 验证码图片 */ + protected byte[] imageBytes; + /** 验证码生成器 */ + protected CodeGenerator generator; + /** 背景色 */ + protected Color background; + /** 文字透明度 */ + protected AlphaComposite textAlpha; + + /** + * 构造,使用随机验证码生成器生成验证码 + * + * @param width 图片宽 + * @param height 图片高 + * @param codeCount 字符个数 + * @param interfereCount 验证码干扰元素个数 + */ + public AbstractCaptcha(int width, int height, int codeCount, int interfereCount) { + this(width, height, new RandomGenerator(codeCount), interfereCount); + } + + /** + * 构造 + * + * @param width 图片宽 + * @param height 图片高 + * @param generator 验证码生成器 + * @param interfereCount 验证码干扰元素个数 + */ + public AbstractCaptcha(int width, int height, CodeGenerator generator, int interfereCount) { + this.width = width; + this.height = height; + this.generator = generator; + this.interfereCount = interfereCount; + // 字体高度设为验证码高度-2,留边距 + this.font = new Font(Font.SANS_SERIF, Font.PLAIN, (int) (this.height * 0.75)); + } + + @Override + public void createCode() { + generateCode(); + + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + ImgUtil.writePng(createImage(this.code), out); + this.imageBytes = out.toByteArray(); + } + + /** + * 生成验证码字符串 + * + * @since 3.3.0 + */ + protected void generateCode() { + this.code = generator.generate(); + } + + /** + * 根据生成的code创建验证码图片 + * + * @param code 验证码 + */ + protected abstract Image createImage(String code); + + @Override + public String getCode() { + if(null == this.code) { + createCode(); + } + return this.code; + } + + @Override + public boolean verify(String userInputCode) { + return this.generator.verify(getCode(), userInputCode); + } + + /** + * 验证码写出到文件 + * + * @param path 文件路径 + * @throws IORuntimeException IO异常 + */ + public void write(String path) throws IORuntimeException { + this.write(FileUtil.touch(path)); + } + + /** + * 验证码写出到文件 + * + * @param file 文件 + * @throws IORuntimeException IO异常 + */ + public void write(File file) throws IORuntimeException { + try (OutputStream out = FileUtil.getOutputStream(file)) { + this.write(out); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + @Override + public void write(OutputStream out) { + IoUtil.write(out, false, getImageBytes()); + } + + /** + * 获取图形验证码图片bytes + * + * @return 图形验证码图片bytes + * @since 4.5.17 + */ + public byte[] getImageBytes() { + if (null == this.imageBytes) { + createCode(); + } + return this.imageBytes; + } + + /** + * 获取验证码图 + * + * @return 验证码图 + */ + public BufferedImage getImage() { + return ImgUtil.read(IoUtil.toStream(getImageBytes())); + } + + /** + * 获得图片的Base64形式 + * + * @return 图片的Base64 + * @since 3.3.0 + */ + public String getImageBase64() { + return Base64.encode(getImageBytes()); + } + + /** + * 自定义字体 + * + * @param font 字体 + */ + public void setFont(Font font) { + this.font = font; + } + + /** + * 获取验证码生成器 + * + * @return 验证码生成器 + */ + public CodeGenerator getGenerator() { + return generator; + } + + /** + * 设置验证码生成器 + * + * @param generator 验证码生成器 + */ + public void setGenerator(CodeGenerator generator) { + this.generator = generator; + } + + /** + * 设置背景色 + * + * @param background 背景色 + * @since 4.1.22 + */ + public void setBackground(Color background) { + this.background = background; + } + + /** + * 设置文字透明度 + * + * @param textAlpha 文字透明度,取值0~1,1表示不透明 + * @since 4.5.17 + */ + public void setTextAlpha(float textAlpha) { + this.textAlpha = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, textAlpha); + } + +} diff --git a/hutool-captcha/src/main/java/cn/hutool/captcha/CaptchaUtil.java b/hutool-captcha/src/main/java/cn/hutool/captcha/CaptchaUtil.java new file mode 100644 index 000000000..eff5aac6a --- /dev/null +++ b/hutool-captcha/src/main/java/cn/hutool/captcha/CaptchaUtil.java @@ -0,0 +1,86 @@ +package cn.hutool.captcha; + +/** + * 图形验证码工具 + * + * @author looly + * @since 3.1.2 + */ +public class CaptchaUtil { + + /** + * 创建线干扰的验证码,默认5位验证码,150条干扰线 + * + * @param width 图片宽 + * @param height 图片高 + * @return {@link LineCaptcha} + */ + public static LineCaptcha createLineCaptcha(int width, int height) { + return new LineCaptcha(width, height); + } + + /** + * 创建线干扰的验证码 + * + * @param width 图片宽 + * @param height 图片高 + * @param codeCount 字符个数 + * @param lineCount 干扰线条数 + * @return {@link LineCaptcha} + */ + public static LineCaptcha createLineCaptcha(int width, int height, int codeCount, int lineCount) { + return new LineCaptcha(width, height, codeCount, lineCount); + } + + /** + * 创建圆圈干扰的验证码,默认5位验证码,15个干扰圈 + * + * @param width 图片宽 + * @param height 图片高 + * @return {@link CircleCaptcha} + * @since 3.2.3 + */ + public static CircleCaptcha createCircleCaptcha(int width, int height) { + return new CircleCaptcha(width, height); + } + + /** + * 创建圆圈干扰的验证码 + * + * @param width 图片宽 + * @param height 图片高 + * @param codeCount 字符个数 + * @param circleCount 干扰圆圈条数 + * @return {@link CircleCaptcha} + * @since 3.2.3 + */ + public static CircleCaptcha createCircleCaptcha(int width, int height, int codeCount, int circleCount) { + return new CircleCaptcha(width, height, codeCount, circleCount); + } + + /** + * 创建扭曲干扰的验证码,默认5位验证码 + * + * @param width 图片宽 + * @param height 图片高 + * @return {@link ShearCaptcha} + * @since 3.2.3 + */ + public static ShearCaptcha createShearCaptcha(int width, int height) { + return new ShearCaptcha(width, height); + } + + /** + * 创建扭曲干扰的验证码,默认5位验证码 + * + * @param width 图片宽 + * @param height 图片高 + * @param codeCount 字符个数 + * @param thickness 干扰线宽度 + * @return {@link ShearCaptcha} + * @since 3.3.0 + */ + public static ShearCaptcha createShearCaptcha(int width, int height, int codeCount, int thickness) { + return new ShearCaptcha(width, height, codeCount, thickness); + } +} diff --git a/hutool-captcha/src/main/java/cn/hutool/captcha/CircleCaptcha.java b/hutool-captcha/src/main/java/cn/hutool/captcha/CircleCaptcha.java new file mode 100644 index 000000000..b8df607f0 --- /dev/null +++ b/hutool-captcha/src/main/java/cn/hutool/captcha/CircleCaptcha.java @@ -0,0 +1,101 @@ +package cn.hutool.captcha; + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.image.BufferedImage; +import java.util.concurrent.ThreadLocalRandom; + +import cn.hutool.core.img.GraphicsUtil; +import cn.hutool.core.img.ImgUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.RandomUtil; + +/** + * 圆圈干扰验证码 + * + * @author looly + * @since 3.2.3 + * + */ +public class CircleCaptcha extends AbstractCaptcha { + private static final long serialVersionUID = -7096627300356535494L; + + /** + * 构造 + * + * @param width 图片宽 + * @param height 图片高 + */ + public CircleCaptcha(int width, int height) { + this(width, height, 5); + } + + /** + * 构造 + * + * @param width 图片宽 + * @param height 图片高 + * @param codeCount 字符个数 + */ + public CircleCaptcha(int width, int height, int codeCount) { + this(width, height, codeCount, 15); + } + + /** + * 构造 + * + * @param width 图片宽 + * @param height 图片高 + * @param codeCount 字符个数 + * @param interfereCount 验证码干扰元素个数 + */ + public CircleCaptcha(int width, int height, int codeCount, int interfereCount) { + super(width, height, codeCount, interfereCount); + } + + @Override + public Image createImage(String code) { + final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + final Graphics2D g = ImgUtil.createGraphics(image, ObjectUtil.defaultIfNull(this.background, Color.WHITE)); + + // 随机画干扰圈圈 + drawInterfere(g); + + // 画字符串 + drawString(g, code); + + return image; + } + + // ----------------------------------------------------------------------------------------------------- Private method start + /** + * 绘制字符串 + * + * @param g {@link Graphics}画笔 + * @param code 验证码 + */ + private void drawString(Graphics2D g, String code) { + // 指定透明度 + if (null != this.textAlpha) { + g.setComposite(this.textAlpha); + } + GraphicsUtil.drawStringColourful(g, code, this.font, this.width, this.height); + } + + /** + * 画随机干扰 + * + * @param g {@link Graphics2D} + */ + private void drawInterfere(Graphics2D g) { + final ThreadLocalRandom random = RandomUtil.getRandom(); + + for (int i = 0; i < this.interfereCount; i++) { + g.setColor(ImgUtil.randomColor(random)); + g.drawOval(random.nextInt(width), random.nextInt(height), random.nextInt(height >> 1), random.nextInt(height >> 1)); + } + } + // ----------------------------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-captcha/src/main/java/cn/hutool/captcha/ICaptcha.java b/hutool-captcha/src/main/java/cn/hutool/captcha/ICaptcha.java new file mode 100644 index 000000000..b98110bab --- /dev/null +++ b/hutool-captcha/src/main/java/cn/hutool/captcha/ICaptcha.java @@ -0,0 +1,40 @@ +package cn.hutool.captcha; + +import java.io.OutputStream; +import java.io.Serializable; + +/** + * 验证码接口,提供验证码对象接口定义 + * + * @author looly + * + */ +public interface ICaptcha extends Serializable{ + + /** + * 创建验证码,实现类需同时生成随机验证码字符串和验证码图片 + */ + void createCode(); + + /** + * 获取验证码的文字内容 + * + * @return 验证码文字内容 + */ + String getCode(); + + /** + * 验证验证码是否正确,建议忽略大小写 + * + * @param userInputCode 用户输入的验证码 + * @return 是否与生成的一直 + */ + boolean verify(String userInputCode); + + /** + * 将验证码写出到目标流中 + * + * @param out 目标流 + */ + void write(OutputStream out); +} diff --git a/hutool-captcha/src/main/java/cn/hutool/captcha/LineCaptcha.java b/hutool-captcha/src/main/java/cn/hutool/captcha/LineCaptcha.java new file mode 100644 index 000000000..1fc66ba63 --- /dev/null +++ b/hutool-captcha/src/main/java/cn/hutool/captcha/LineCaptcha.java @@ -0,0 +1,96 @@ +package cn.hutool.captcha; + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.image.BufferedImage; +import java.util.concurrent.ThreadLocalRandom; + +import cn.hutool.core.img.GraphicsUtil; +import cn.hutool.core.img.ImgUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.RandomUtil; + +/** + * 使用干扰线方式生成的图形验证码 + * + * @author looly + * @since 3.1.2 + */ +public class LineCaptcha extends AbstractCaptcha { + private static final long serialVersionUID = 8691294460763091089L; + + // -------------------------------------------------------------------- Constructor start + /** + * 构造,默认5位验证码,150条干扰线 + * + * @param width 图片宽 + * @param height 图片高 + */ + public LineCaptcha(int width, int height) { + this(width, height, 5, 150); + } + + /** + * 构造 + * + * @param width 图片宽 + * @param height 图片高 + * @param codeCount 字符个数 + * @param lineCount 干扰线条数 + */ + public LineCaptcha(int width, int height, int codeCount, int lineCount) { + super(width, height, codeCount, lineCount); + } + // -------------------------------------------------------------------- Constructor end + + @Override + public Image createImage(String code) { + // 图像buffer + final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + final Graphics2D g = GraphicsUtil.createGraphics(image, ObjectUtil.defaultIfNull(this.background, Color.WHITE)); + + // 干扰线 + drawInterfere(g); + + // 字符串 + drawString(g, code); + + return image; + } + + // ----------------------------------------------------------------------------------------------------- Private method start + /** + * 绘制字符串 + * + * @param g {@link Graphics}画笔 + * @param code 验证码 + */ + private void drawString(Graphics2D g, String code) { + // 指定透明度 + if (null != this.textAlpha) { + g.setComposite(this.textAlpha); + } + GraphicsUtil.drawStringColourful(g, code, this.font, this.width, this.height); + } + + /** + * 绘制干扰线 + * + * @param g {@link Graphics2D}画笔 + */ + private void drawInterfere(Graphics2D g) { + final ThreadLocalRandom random = RandomUtil.getRandom(); + // 干扰线 + for (int i = 0; i < this.interfereCount; i++) { + int xs = random.nextInt(width); + int ys = random.nextInt(height); + int xe = xs + random.nextInt(width / 8); + int ye = ys + random.nextInt(height / 8); + g.setColor(ImgUtil.randomColor(random)); + g.drawLine(xs, ys, xe, ye); + } + } + // ----------------------------------------------------------------------------------------------------- Private method start +} \ No newline at end of file diff --git a/hutool-captcha/src/main/java/cn/hutool/captcha/ShearCaptcha.java b/hutool-captcha/src/main/java/cn/hutool/captcha/ShearCaptcha.java new file mode 100644 index 000000000..41e3bea49 --- /dev/null +++ b/hutool-captcha/src/main/java/cn/hutool/captcha/ShearCaptcha.java @@ -0,0 +1,201 @@ +package cn.hutool.captcha; + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.image.BufferedImage; + +import cn.hutool.core.img.GraphicsUtil; +import cn.hutool.core.img.ImgUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.RandomUtil; + +/** + * 扭曲干扰验证码 + * + * @author looly + * @since 3.2.3 + * + */ +public class ShearCaptcha extends AbstractCaptcha { + private static final long serialVersionUID = -7096627300356535494L; + + /** + * 构造 + * + * @param width 图片宽 + * @param height 图片高 + */ + public ShearCaptcha(int width, int height) { + this(width, height, 5); + } + + /** + * 构造 + * + * @param width 图片宽 + * @param height 图片高 + * @param codeCount 字符个数 + */ + public ShearCaptcha(int width, int height, int codeCount) { + this(width, height, codeCount, 4); + } + + /** + * 构造 + * + * @param width 图片宽 + * @param height 图片高 + * @param codeCount 字符个数 + * @param thickness 干扰线宽度 + */ + public ShearCaptcha(int width, int height, int codeCount, int thickness) { + super(width, height, codeCount, thickness); + } + + @Override + public Image createImage(String code) { + final BufferedImage image = new BufferedImage(this.width, this.height, BufferedImage.TYPE_INT_RGB); + final Graphics2D g = GraphicsUtil.createGraphics(image, ObjectUtil.defaultIfNull(this.background, Color.WHITE)); + + // 画字符串 + drawString(g, code); + + // 扭曲 + shear(g, this.width, this.height, ObjectUtil.defaultIfNull(this.background, Color.WHITE)); + // 画干扰线 + drawInterfere(g, 0, RandomUtil.randomInt(this.height) + 1, this.width, RandomUtil.randomInt(this.height) + 1, this.interfereCount, ImgUtil.randomColor()); + + return image; + } + + // ----------------------------------------------------------------------------------------------------- Private method start + /** + * 绘制字符串 + * + * @param g {@link Graphics}画笔 + * @param code 验证码 + */ + private void drawString(Graphics2D g, String code) { + // 指定透明度 + if (null != this.textAlpha) { + g.setComposite(this.textAlpha); + } + GraphicsUtil.drawStringColourful(g, code, this.font, this.width, this.height); + } + + /** + * 扭曲 + * + * @param g {@link Graphics} + * @param w1 w1 + * @param h1 h1 + * @param color 颜色 + */ + private void shear(Graphics g, int w1, int h1, Color color) { + shearX(g, w1, h1, color); + shearY(g, w1, h1, color); + } + + /** + * X坐标扭曲 + * + * @param g {@link Graphics} + * @param w1 宽 + * @param h1 高 + * @param color 颜色 + */ + private void shearX(Graphics g, int w1, int h1, Color color) { + + int period = RandomUtil.randomInt(this.width); + + boolean borderGap = true; + int frames = 1; + int phase = RandomUtil.randomInt(2); + + for (int i = 0; i < h1; i++) { + double d = (double) (period >> 1) * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames); + g.copyArea(0, i, w1, 1, (int) d, 0); + if (borderGap) { + g.setColor(color); + g.drawLine((int) d, i, 0, i); + g.drawLine((int) d + w1, i, w1, i); + } + } + + } + + /** + * Y坐标扭曲 + * + * @param g {@link Graphics} + * @param w1 宽 + * @param h1 高 + * @param color 颜色 + */ + private void shearY(Graphics g, int w1, int h1, Color color) { + + int period = RandomUtil.randomInt(this.height >> 1); + + int frames = 20; + int phase = 7; + for (int i = 0; i < w1; i++) { + double d = (double) (period >> 1) * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames); + g.copyArea(i, 0, 1, h1, 0, (int) d); + g.setColor(color); + // 擦除原位置的痕迹 + g.drawLine(i, (int) d, i, 0); + g.drawLine(i, (int) d + h1, i, h1); + } + + } + + /** + * 干扰线 + * + * @param g {@link Graphics} + * @param x1x1 + * @param y1 y1 + * @param x2 x2 + * @param y2 y2 + * @param thickness 粗细 + * @param c 颜色 + */ + private void drawInterfere(Graphics g, int x1, int y1, int x2, int y2, int thickness, Color c) { + + // The thick line is in fact a filled polygon + g.setColor(c); + int dX = x2 - x1; + int dY = y2 - y1; + // line length + double lineLength = Math.sqrt(dX * dX + dY * dY); + + double scale = (double) (thickness) / (2 * lineLength); + + // The x and y increments from an endpoint needed to create a + // rectangle... + double ddx = -scale * (double) dY; + double ddy = scale * (double) dX; + ddx += (ddx > 0) ? 0.5 : -0.5; + ddy += (ddy > 0) ? 0.5 : -0.5; + int dx = (int) ddx; + int dy = (int) ddy; + + // Now we can compute the corner points... + int xPoints[] = new int[4]; + int yPoints[] = new int[4]; + + xPoints[0] = x1 + dx; + yPoints[0] = y1 + dy; + xPoints[1] = x1 - dx; + yPoints[1] = y1 - dy; + xPoints[2] = x2 - dx; + yPoints[2] = y2 - dy; + xPoints[3] = x2 + dx; + yPoints[3] = y2 + dy; + + g.fillPolygon(xPoints, yPoints, 4); + } + // ----------------------------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-captcha/src/main/java/cn/hutool/captcha/generator/AbstractGenerator.java b/hutool-captcha/src/main/java/cn/hutool/captcha/generator/AbstractGenerator.java new file mode 100644 index 000000000..c9a6fff66 --- /dev/null +++ b/hutool-captcha/src/main/java/cn/hutool/captcha/generator/AbstractGenerator.java @@ -0,0 +1,48 @@ +package cn.hutool.captcha.generator; + +import cn.hutool.core.util.RandomUtil; + +/** + * 随机字符验证码生成器
+ * 可以通过传入的基础集合和长度随机生成验证码字符 + * + * @author looly + * @since 4.1.2 + */ +public abstract class AbstractGenerator implements CodeGenerator { + private static final long serialVersionUID = 8685744597154953479L; + + /** 基础字符集合,用于随机获取字符串的字符集合 */ + protected String baseStr; + /** 验证码长度 */ + protected int length; + + /** + * 构造,使用字母+数字做为基础 + * + * @param count 生成验证码长度 + */ + public AbstractGenerator(int count) { + this(RandomUtil.BASE_CHAR_NUMBER, count); + } + + /** + * 构造 + * + * @param baseStr 基础字符集合,用于随机获取字符串的字符集合 + * @param length 生成验证码长度 + */ + public AbstractGenerator(String baseStr, int length) { + this.baseStr = baseStr; + this.length = length; + } + + /** + * 获取长度验证码 + * + * @return 验证码长度 + */ + public int getLength() { + return this.length; + } +} diff --git a/hutool-captcha/src/main/java/cn/hutool/captcha/generator/CodeGenerator.java b/hutool-captcha/src/main/java/cn/hutool/captcha/generator/CodeGenerator.java new file mode 100644 index 000000000..87b02beb8 --- /dev/null +++ b/hutool-captcha/src/main/java/cn/hutool/captcha/generator/CodeGenerator.java @@ -0,0 +1,28 @@ +package cn.hutool.captcha.generator; + +import java.io.Serializable; + +/** + * 验证码文字生成器 + * + * @author looly + * @since 4.1.2 + */ +public interface CodeGenerator extends Serializable{ + /** + * 生成验证码 + * + * @return 验证码 + */ + public String generate(); + + /** + * 验证用户输入的字符串是否与生成的验证码匹配
+ * 用户通过实现此方法定义验证码匹配方式 + * + * @param code 生成的随机验证码 + * @param userInputCode 用户输入的验证码 + * @return 是否验证通过 + */ + public boolean verify(String code, String userInputCode); +} diff --git a/hutool-captcha/src/main/java/cn/hutool/captcha/generator/MathGenerator.java b/hutool-captcha/src/main/java/cn/hutool/captcha/generator/MathGenerator.java new file mode 100644 index 000000000..3c8c178de --- /dev/null +++ b/hutool-captcha/src/main/java/cn/hutool/captcha/generator/MathGenerator.java @@ -0,0 +1,96 @@ +package cn.hutool.captcha.generator; + +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 数字计算验证码生成器 + * + * @author looly + * @since 4.1.2 + */ +public class MathGenerator implements CodeGenerator { + private static final long serialVersionUID = -5514819971774091076L; + + private static final String operators = "+-*"; + + /** 参与计算数字最大长度 */ + private int numberLength; + + /** + * 构造 + */ + public MathGenerator() { + this(2); + } + + /** + * 构造 + * + * @param numberLength 参与计算最大数字位数 + */ + public MathGenerator(int numberLength) { + this.numberLength = numberLength; + } + + @Override + public String generate() { + final int limit = getLimit(); + String number1 = Integer.toString(RandomUtil.randomInt(limit)); + String number2 = Integer.toString(RandomUtil.randomInt(limit)); + number1 = StrUtil.padAfter(number1, this.numberLength, CharUtil.SPACE); + number2 = StrUtil.padAfter(number2, this.numberLength, CharUtil.SPACE); + + final String code = StrUtil.builder()// + .append(number1)// + .append(RandomUtil.randomChar(operators))// + .append(number2)// + .append('=').toString(); + return code; + } + + @Override + public boolean verify(String code, String userInputCode) { + int result; + try { + result = Integer.parseInt(userInputCode); + } catch (NumberFormatException e) { + // 用户输入非数字 + return false; + } + + final int a = Integer.parseInt(StrUtil.sub(code, 0, this.numberLength).trim()); + final char operator = code.charAt(this.numberLength); + final int b = Integer.parseInt(StrUtil.sub(code, this.numberLength + 1, this.numberLength + 1 + this.numberLength).trim()); + + switch (operator) { + case '+': + return (a + b) == result; + case '-': + return (a - b) == result; + case '*': + return (a * b) == result; + default: + return false; + } + } + + /** + * 获取长度验证码 + * + * @return 验证码长度 + */ + public int getLength() { + return this.numberLength * 2 + 2; + } + + /** + * 根据长度获取参与计算数字最大值 + * + * @return 最大值 + */ + private int getLimit() { + return Integer.parseInt("1" + StrUtil.repeat('0', this.numberLength)); + } +} diff --git a/hutool-captcha/src/main/java/cn/hutool/captcha/generator/RandomGenerator.java b/hutool-captcha/src/main/java/cn/hutool/captcha/generator/RandomGenerator.java new file mode 100644 index 000000000..0bff98fe4 --- /dev/null +++ b/hutool-captcha/src/main/java/cn/hutool/captcha/generator/RandomGenerator.java @@ -0,0 +1,47 @@ +package cn.hutool.captcha.generator; + +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 随机字符验证码生成器
+ * 可以通过传入的基础集合和长度随机生成验证码字符 + * + * @author looly + * @since 4.1.2 + */ +public class RandomGenerator extends AbstractGenerator { + private static final long serialVersionUID = -7802758587765561876L; + + /** + * 构造,使用字母+数字做为基础 + * + * @param count 生成验证码长度 + */ + public RandomGenerator(int count) { + super(count); + } + + /** + * 构造 + * + * @param baseStr 基础字符集合,用于随机获取字符串的字符集合 + * @param length 生成验证码长度 + */ + public RandomGenerator(String baseStr, int length) { + super(baseStr, length); + } + + @Override + public String generate() { + return RandomUtil.randomString(this.baseStr, this.length); + } + + @Override + public boolean verify(String code, String userInputCode) { + if (StrUtil.isNotBlank(userInputCode)) { + return StrUtil.equalsIgnoreCase(code, userInputCode); + } + return false; + } +} diff --git a/hutool-captcha/src/main/java/cn/hutool/captcha/generator/package-info.java b/hutool-captcha/src/main/java/cn/hutool/captcha/generator/package-info.java new file mode 100644 index 000000000..d7f2a4167 --- /dev/null +++ b/hutool-captcha/src/main/java/cn/hutool/captcha/generator/package-info.java @@ -0,0 +1,7 @@ +/** + * 验证码生成策略实现 + * + * @author looly + * @since 4.1.2 + */ +package cn.hutool.captcha.generator; \ No newline at end of file diff --git a/hutool-captcha/src/main/java/cn/hutool/captcha/package-info.java b/hutool-captcha/src/main/java/cn/hutool/captcha/package-info.java new file mode 100644 index 000000000..857421c42 --- /dev/null +++ b/hutool-captcha/src/main/java/cn/hutool/captcha/package-info.java @@ -0,0 +1,7 @@ +/** + * 图片验证码实现 + * + * @author looly + * + */ +package cn.hutool.captcha; \ No newline at end of file diff --git a/hutool-captcha/src/test/java/cn/hutool/captcha/CaptchaTest.java b/hutool-captcha/src/test/java/cn/hutool/captcha/CaptchaTest.java new file mode 100644 index 000000000..8c79327a5 --- /dev/null +++ b/hutool-captcha/src/test/java/cn/hutool/captcha/CaptchaTest.java @@ -0,0 +1,95 @@ +package cn.hutool.captcha; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.captcha.generator.MathGenerator; +import cn.hutool.core.lang.Console; + +/** + * 直线干扰验证码单元测试 + * + * @author looly + * + */ +public class CaptchaTest { + + @Test + public void lineCaptchaTest1() { + // 定义图形验证码的长和宽 + LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 100); + Assert.assertNotNull(lineCaptcha.getCode()); + Assert.assertTrue(lineCaptcha.verify(lineCaptcha.getCode())); + } + + @Test + @Ignore + public void lineCaptchaWithMathTest() { + // 定义图形验证码的长和宽 + LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 80); + lineCaptcha.setGenerator(new MathGenerator()); + lineCaptcha.setTextAlpha(0.8f); + lineCaptcha.write("f:/captcha/math.png"); + } + + @Test + @Ignore + public void lineCaptchaTest2() { + + // 定义图形验证码的长和宽 + LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 100); + // LineCaptcha lineCaptcha = new LineCaptcha(200, 100, 4, 150); + // 图形验证码写出,可以写出到文件,也可以写出到流 + lineCaptcha.write("f:/captcha/line.png"); + Console.log(lineCaptcha.getCode()); + // 验证图形验证码的有效性,返回boolean值 + lineCaptcha.verify("1234"); + + lineCaptcha.createCode(); + lineCaptcha.write("f:/captcha/line2.png"); + Console.log(lineCaptcha.getCode()); + // 验证图形验证码的有效性,返回boolean值 + lineCaptcha.verify("1234"); + } + + @Test + @Ignore + public void circleCaptchaTest() { + + // 定义图形验证码的长和宽 + CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(200, 100, 4, 20); + // CircleCaptcha captcha = new CircleCaptcha(200, 100, 4, 20); + // 图形验证码写出,可以写出到文件,也可以写出到流 + captcha.write("f:/captcha/circle.png"); + // 验证图形验证码的有效性,返回boolean值 + captcha.verify("1234"); + } + + @Test + @Ignore + public void ShearCaptchaTest() { + + // 定义图形验证码的长和宽 + ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(203, 100, 4, 4); + // ShearCaptcha captcha = new ShearCaptcha(200, 100, 4, 4); + // 图形验证码写出,可以写出到文件,也可以写出到流 + captcha.write("f:/captcha/shear.png"); + // 验证图形验证码的有效性,返回boolean值 + captcha.verify("1234"); + } + + @Test + @Ignore + public void ShearCaptchaWithMathTest() { + + // 定义图形验证码的长和宽 + ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(200, 45, 4, 4); + captcha.setGenerator(new MathGenerator()); + // ShearCaptcha captcha = new ShearCaptcha(200, 100, 4, 4); + // 图形验证码写出,可以写出到文件,也可以写出到流 + captcha.write("f:/captcha/shear_math.png"); + // 验证图形验证码的有效性,返回boolean值 + captcha.verify("1234"); + } +} diff --git a/hutool-captcha/src/test/java/cn/hutool/captcha/CaptchaUtilTest.java b/hutool-captcha/src/test/java/cn/hutool/captcha/CaptchaUtilTest.java new file mode 100644 index 000000000..1b77711a6 --- /dev/null +++ b/hutool-captcha/src/test/java/cn/hutool/captcha/CaptchaUtilTest.java @@ -0,0 +1,15 @@ +package cn.hutool.captcha; + +import org.junit.Ignore; +import org.junit.Test; + +public class CaptchaUtilTest { + + @Test + @Ignore + public void createTest() { + for(int i = 0; i < 1; i++) { + CaptchaUtil.createShearCaptcha(320, 240); + } + } +} diff --git a/hutool-core/pom.xml b/hutool-core/pom.xml new file mode 100644 index 000000000..2c90d4923 --- /dev/null +++ b/hutool-core/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + + jar + + + cn.hutool + hutool-parent + 4.6.2-SNAPSHOT + + + hutool-core + ${project.artifactId} + Hutool核心 + + diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationUtil.java b/hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationUtil.java new file mode 100644 index 000000000..dc7b666cf --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/AnnotationUtil.java @@ -0,0 +1,196 @@ +package cn.hutool.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.lang.Filter; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ReflectUtil; + +/** + * 注解工具类
+ * 快速获取注解对象、注解值等工具封装 + * + * @author looly + * @since 4.0.9 + */ +public class AnnotationUtil { + + /** + * 将指定的被注解的元素转换为组合注解元素 + * + * @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 + * @return 注解对象 + */ + public static Annotation[] getAnnotations(AnnotatedElement annotationEle, boolean isCombination) { + return (null == annotationEle) ? null : (isCombination ? toCombination(annotationEle) : annotationEle).getAnnotations(); + } + + /** + * 获取指定注解 + * + * @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); + } + + /** + * 获取指定注解默认值
+ * 如果无指定的属性方法返回null + * + * @param 注解值类型 + * @param annotationEle {@link AccessibleObject},可以是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 AccessibleObject},可以是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 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, new Filter() { + @Override + public boolean accept(Method t) { + if (ArrayUtil.isEmpty(t.getParameterTypes())) { + // 只读取无参方法 + final String name = t.getName(); + if ("hashCode".equals(name) || "toString".equals(name) || "annotationType".equals(name)) { + // 跳过自有的几个方法 + return false; + } + return true; + } + 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); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/CombinationAnnotationElement.java b/hutool-core/src/main/java/cn/hutool/core/annotation/CombinationAnnotationElement.java new file mode 100644 index 000000000..380405610 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/annotation/CombinationAnnotationElement.java @@ -0,0 +1,127 @@ +package cn.hutool.core.annotation; + +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.lang.reflect.AnnotatedElement; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import cn.hutool.core.collection.CollUtil; + +/** + * 组合注解 对JDK的原生注解机制做一个增强,支持类似Spring的组合注解。
+ * 核心实现使用了递归获取指定元素上的注解以及注解的注解,以实现复合注解的获取。 + * + * @author Succy,Looly + * @since 4.0.9 + **/ + +public class CombinationAnnotationElement implements AnnotatedElement, Serializable { + private static final long serialVersionUID = 1L; + + /** 元注解 */ + private static final Set> META_ANNOTATIONS = CollUtil.newHashSet(Target.class, // + Retention.class, // + Inherited.class, // + Documented.class, // + SuppressWarnings.class, // + Override.class, // + Deprecated.class// + ); + + /** 注解类型与注解对象对应表 */ + private Map, Annotation> annotationMap; + /** 直接注解类型与注解对象对应表 */ + private Map, Annotation> declaredAnnotationMap; + + /** + * 构造 + * + * @param element 需要解析注解的元素:可以是Class、Method、Field、Constructor、ReflectPermission + */ + public CombinationAnnotationElement(AnnotatedElement element) { + 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[annotations.size()]); + } + + @Override + public Annotation[] getDeclaredAnnotations() { + final Collection annotations = this.declaredAnnotationMap.values(); + return annotations.toArray(new Annotation[annotations.size()]); + } + + /** + * 初始化 + * + * @param element 元素 + */ + private void init(AnnotatedElement element) { + Annotation[] declaredAnnotations = element.getDeclaredAnnotations(); + this.declaredAnnotationMap = new HashMap<>(); + parseDeclared(declaredAnnotations); + + Annotation[] annotations = element.getAnnotations(); + if(declaredAnnotations == annotations) { + this.annotationMap = this.declaredAnnotationMap; + }else { + this.annotationMap = new HashMap<>(); + parse(annotations); + } + } + + /** + * 进行递归解析注解,直到全部都是元注解为止 + * + * @param annotations Class, Method, Field等 + */ + private void parseDeclared(Annotation[] annotations) { + Class annotationType; + // 直接注解 + for (Annotation annotation : annotations) { + annotationType = annotation.annotationType(); + if (false == META_ANNOTATIONS.contains(annotationType)) { + declaredAnnotationMap.put(annotationType, annotation); + parseDeclared(annotationType.getDeclaredAnnotations()); + } + } + } + + /** + * 进行递归解析注解,直到全部都是元注解为止 + * + * @param element Class, Method, Field等 + */ + private void parse(Annotation[] annotations) { + Class annotationType; + for (Annotation annotation : annotations) { + annotationType = annotation.annotationType(); + if (false == META_ANNOTATIONS.contains(annotationType)) { + annotationMap.put(annotationType, annotation); + parse(annotationType.getAnnotations()); + } + } + } +} \ No newline at end of file diff --git a/hutool-core/src/main/java/cn/hutool/core/annotation/package-info.java b/hutool-core/src/main/java/cn/hutool/core/annotation/package-info.java new file mode 100644 index 000000000..a9746ec48 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/bean/BeanDesc.java b/hutool-core/src/main/java/cn/hutool/core/bean/BeanDesc.java new file mode 100644 index 000000000..2cd80ab54 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/bean/BeanDesc.java @@ -0,0 +1,458 @@ +package cn.hutool.core.bean; + +import java.io.Serializable; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.CaseInsensitiveMap; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ModifierUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.TypeUtil; + +/** + * 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 Class beanClass; + /** 属性Map */ + private 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() { + for (Field field : ReflectUtil.getFields(this.beanClass)) { + if(false == ModifierUtil.isStatic(field)) { + //只针对非static属性 + this.propMap.put(field.getName(), createProp(field)); + } + } + return this; + } + + /** + * 根据字段创建属性描述
+ * 查找Getter和Setter方法时会: + * + *
+	 * 1. 忽略字段和方法名的大小写
+	 * 2. Getter查找getXXX、isXXX、getIsXXX
+	 * 3. Setter查找setXXX、setIsXXX
+	 * 4. Setter忽略参数值与字段值不匹配的情况,因此有多个参数类型的重载时,会调用首次匹配的
+	 * 
+ * + * @param field 字段 + * @return {@link PropDesc} + * @since 4.0.2 + */ + private PropDesc createProp(Field field) { + final String fieldName = field.getName(); + final Class fieldType = field.getType(); + final boolean isBooeanField = BooleanUtil.isBoolean(fieldType); + + Method getter = null; + Method setter = null; + + String methodName; + Class[] parameterTypes; + for (Method method : ReflectUtil.getMethods(this.beanClass)) { + parameterTypes = method.getParameterTypes(); + if (parameterTypes.length > 1) { + // 多于1个参数说明非Getter或Setter + continue; + } + + methodName = method.getName(); + if (parameterTypes.length == 0) { + // 无参数,可能为Getter方法 + if (isMatchGetter(methodName, fieldName, isBooeanField)) { + // 方法名与字段名匹配,则为Getter方法 + getter = method; + } + } else if (isMatchSetter(methodName, fieldName, isBooeanField)) { + // 只有一个参数的情况下方法名与字段名对应匹配,则为Setter方法 + 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 isBooeanField 是否为Boolean类型字段 + * @return 是否匹配 + */ + private boolean isMatchGetter(String methodName, String fieldName, boolean isBooeanField) { + // 全部转为小写,忽略大小写比较 + methodName = methodName.toLowerCase(); + fieldName = fieldName.toLowerCase(); + + if (false == methodName.startsWith("get") && false == methodName.startsWith("is")) { + // 非标准Getter方法 + return false; + } + if("getclass".equals(methodName)) { + //跳过getClass方法 + return false; + } + + // 针对Boolean类型特殊检查 + if (isBooeanField) { + if (fieldName.startsWith("is")) { + // 字段已经是is开头 + if (methodName.equals(fieldName) // isName -》 isName + || methodName.equals("get" + fieldName)// isName -》 getIsName + || methodName.equals("is" + fieldName)// isName -》 isIsName + ) { + return true; + } + } else if (methodName.equals("is" + fieldName)) { + // 字段非is开头, name -》 isName + return true; + } + } + + // 包括boolean的任何类型只有一种匹配情况:name -》 getName + return methodName.equals("get" + fieldName); + } + + /** + * 方法是否为Setter方法
+ * 匹配规则如下(忽略大小写): + * + *
+	 * 字段名    -》 方法名
+	 * isName  -》 setName
+	 * isName  -》 setIsName
+	 * name     -》 setName
+	 * 
+ * + * @param methodName 方法名 + * @param fieldName 字段名 + * @param isBooeanField 是否为Boolean类型字段 + * @return 是否匹配 + */ + private boolean isMatchSetter(String methodName, String fieldName, boolean isBooeanField) { + // 全部转为小写,忽略大小写比较 + methodName = methodName.toLowerCase(); + fieldName = fieldName.toLowerCase(); + + // 非标准Setter方法跳过 + if (false == methodName.startsWith("set")) { + return false; + } + + // 针对Boolean类型特殊检查 + if (isBooeanField && fieldName.startsWith("is")) { + // 字段是is开头 + if (methodName.equals("set" + StrUtil.removePrefix(fieldName, "is"))// isName -》 setName + || methodName.equals("set" + fieldName)// isName -》 setIsName + ) { + return true; + } + } + + // 包括boolean的任何类型只有一种匹配情况:name -》 setName + return methodName.equals("set" + fieldName); + } + // ------------------------------------------------------------------------------------------------------ Private method end + + /** + * 属性描述 + * + * @author looly + * + */ + public static class PropDesc { + + /** 字段 */ + private Field field; + /** Getter方法 */ + private Method getter; + /** Setter方法 */ + private 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); + } + + /** + * 获取字段名 + * + * @return 字段名 + */ + public String getFieldName() { + 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; + } + + /** + * 获取字段值
+ * 首先调用字段对应的Getter方法获取值,如果Getter方法不存在,则判断字段如果为public,则直接获取字段值 + * + * @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; + } + + /** + * 设置Bean的字段值
+ * 首先调用字段对应的Setter方法,如果Setter方法不存在,则判断字段如果为public,则直接赋值字段值 + * + * @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; + } + + //------------------------------------------------------------------------------------ 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; + } + //------------------------------------------------------------------------------------ Private method end + } +} \ No newline at end of file diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/BeanDescCache.java b/hutool-core/src/main/java/cn/hutool/core/bean/BeanDescCache.java new file mode 100644 index 000000000..4f2965e1a --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/bean/BeanDescCache.java @@ -0,0 +1,33 @@ +package cn.hutool.core.bean; + +import cn.hutool.core.lang.SimpleCache; + +/** + * Bean属性缓存
+ * 缓存用于防止多次反射造成的性能问题 + * @author Looly + * + */ +public enum BeanDescCache { + INSTANCE; + + private SimpleCache, BeanDesc> bdCache = new SimpleCache<>(); + + /** + * 获得属性名和{@link BeanDesc}Map映射 + * @param beanClass Bean的类 + * @return 属性名和{@link BeanDesc}映射 + */ + public BeanDesc getBeanDesc(Class beanClass){ + return bdCache.get(beanClass); + } + + /** + * 加入缓存 + * @param beanClass Bean的类 + * @param BeanDesc 属性名和{@link BeanDesc}映射 + */ + public void putBeanDesc(Class beanClass, BeanDesc BeanDesc){ + bdCache.put(beanClass, BeanDesc); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/BeanException.java b/hutool-core/src/main/java/cn/hutool/core/bean/BeanException.java new file mode 100644 index 000000000..ffc444931 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/bean/BeanInfoCache.java b/hutool-core/src/main/java/cn/hutool/core/bean/BeanInfoCache.java new file mode 100644 index 000000000..a4a0d5f7f --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/bean/BeanInfoCache.java @@ -0,0 +1,39 @@ +package cn.hutool.core.bean; + +import java.beans.PropertyDescriptor; +import java.util.Map; + +import cn.hutool.core.lang.SimpleCache; + +/** + * Bean属性缓存
+ * 缓存用于防止多次反射造成的性能问题 + * @author Looly + * + */ +public enum BeanInfoCache { + INSTANCE; + + private SimpleCache, Map> pdCache = new SimpleCache<>(); + private SimpleCache, Map> ignoreCasePdCache = new SimpleCache<>(); + + /** + * 获得属性名和{@link PropertyDescriptor}Map映射 + * @param beanClass Bean的类 + * @param ignoreCase 是否忽略大小写 + * @return 属性名和{@link PropertyDescriptor}Map映射 + */ + public Map getPropertyDescriptorMap(Class beanClass, boolean ignoreCase){ + return (ignoreCase ? ignoreCasePdCache : pdCache).get(beanClass); + } + + /** + * 加入缓存 + * @param beanClass Bean的类 + * @param fieldNamePropertyDescriptorMap 属性名和{@link PropertyDescriptor}Map映射 + * @param ignoreCase 是否忽略大小写 + */ + public void putPropertyDescriptorMap(Class beanClass, Map fieldNamePropertyDescriptorMap, boolean ignoreCase){ + (ignoreCase ? ignoreCasePdCache : pdCache).put(beanClass, fieldNamePropertyDescriptorMap); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/BeanPath.java b/hutool-core/src/main/java/cn/hutool/core/bean/BeanPath.java new file mode 100644 index 000000000..7b09d9f84 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/bean/BeanPath.java @@ -0,0 +1,295 @@ +package cn.hutool.core.bean; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.text.StrBuilder; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 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[] expChars = { 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 {@link BeanPath} + */ + public static BeanPath create(String expression) { + return new BeanPath(expression); + } + + /** + * 构造 + * + * @param expression 表达式 + */ + public BeanPath(String expression) { + init(expression); + } + + /** + * 获取Bean中对应表达式的值 + * + * @param bean Bean对象或Map或List等 + * @return 值,如果对应值不存在,则返回null + */ + public Object get(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(Object bean, Object value) { + set(bean, this.patternParts, value); + } + + /** + * 设置表达式指定位置(或filed对应)的值
+ * 若表达式指向一个List则设置其坐标对应位置的值,若指向Map则put对应key的值,Bean则设置字段的值
+ * 注意: + * + *
+	 * 1. 如果为List,如果下标不大于List长度,则替换原有值,否则追加值
+	 * 2. 如果为数组,如果下标不大于数组长度,则替换原有值,否则追加值
+	 * 
+ * + * @param bean Bean、Map或List + * @param patternParts 表达式块列表 + * @param value 值 + */ + private void set(Object bean, List patternParts, Object value) { + Object subBean = get(patternParts, bean, true); + if(null == subBean) { + set(bean, patternParts.subList(0, patternParts.size() - 1), new HashMap<>()); + //set中有可能做过转换,因此此处重新获取bean + subBean = get(patternParts, bean, true); + } + BeanUtil.setFieldValue(subBean, patternParts.get(patternParts.size() - 1), value); + } + + // ------------------------------------------------------------------------------------------------------------------------------------- Private method start + /** + * 获取Bean中对应表达式的值 + * + * @param patternParts 表达式分段列表 + * @param bean Bean对象或Map或List等 + * @param ignoreLast 是否忽略最后一个值,忽略最后一个值则用于set,否则用于read + * @return 值,如果对应值不存在,则返回null + */ + private Object get(List patternParts, Object bean, 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 && false == this.isStartWith$ && BeanUtil.isMatchName(bean, patternPart, true)) { + subBean = bean; + isFirst = false; + } else { + return null; + } + } + } + return subBean; + } + + @SuppressWarnings("unchecked") + private static Object getFieldValue(Object bean, String expression) { + if (StrUtil.isBlank(expression)) { + return null; + } + + if (StrUtil.contains(expression, ':')) { + // [start:end:step] 模式 + final List parts = StrUtil.splitTrim(expression, ':'); + int start = Integer.parseInt(parts.get(0)); + 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[] unwrapedKeys = new String[keys.size()]; + for (int i = 0; i < unwrapedKeys.length; i++) { + unwrapedKeys[i] = StrUtil.unWrap(keys.get(i), '\''); + } + if (bean instanceof Map) { + // 只支持String为key的Map + MapUtil.getAny((Map) bean, unwrapedKeys); + } else { + final Map map = BeanUtil.beanToMap(bean); + MapUtil.getAny(map, unwrapedKeys); + } + } + } else { + // 数字或普通字符串 + return BeanUtil.getFieldValue(bean, expression); + } + + return null; + } + + /** + * 初始化 + * + * @param expression 表达式 + */ + private void init(String expression) { + List localPatternParts = new ArrayList<>(); + int length = expression.length(); + + final StrBuilder builder = StrUtil.strBuilder(); + char c; + boolean isNumStart = false;// 下标标识符开始 + for (int i = 0; i < length; i++) { + c = expression.charAt(i); + if (0 == i && '$' == c) { + // 忽略开头的$符,表示当前对象 + isStartWith$ = true; + continue; + } + + if (ArrayUtil.contains(expChars, c)) { + // 处理边界符号 + if (CharUtil.BRACKET_END == c) { + // 中括号(数字下标)结束 + if (false == isNumStart) { + throw new IllegalArgumentException(StrUtil.format("Bad expression '{}':{}, we find ']' but no '[' !", expression, i)); + } + isNumStart = false; + // 中括号结束加入下标 + if (builder.length() > 0) { + localPatternParts.add(unWrapIfPossible(builder)); + } + builder.reset(); + } 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(unWrapIfPossible(builder)); + } + builder.reset(); + } + } 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(unWrapIfPossible(builder)); + } + } + + // 不可变List + this.patternParts = Collections.unmodifiableList(localPatternParts); + } + + /** + * 对于非表达式去除单引号 + * + * @param expression 表达式 + * @return 表达式 + */ + private static String unWrapIfPossible(CharSequence expression) { + if (StrUtil.containsAny(expression, " = ", " > ", " < ", " like ", ",")) { + return expression.toString(); + } + return StrUtil.unWrap(expression, '\''); + } + // ------------------------------------------------------------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/BeanUtil.java b/hutool-core/src/main/java/cn/hutool/core/bean/BeanUtil.java new file mode 100644 index 000000000..5ec4d432f --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/bean/BeanUtil.java @@ -0,0 +1,714 @@ +package cn.hutool.core.bean; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.beans.PropertyEditor; +import java.beans.PropertyEditorManager; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import cn.hutool.core.bean.BeanDesc.PropDesc; +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.convert.Convert; +import cn.hutool.core.lang.Editor; +import cn.hutool.core.lang.Filter; +import cn.hutool.core.map.CaseInsensitiveMap; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; + +/** + * Bean工具类 + * + *

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

+ * + * @author Looly + * @since 3.1.2 + */ +public class BeanUtil { + + /** + * 判断是否为Bean对象
+ * 判定方法是是否存在只有一个参数的setXXX方法 + * + * @param clazz 待测试类 + * @return 是否为Bean对象 + * @see #hasSetter(Class) + */ + public static boolean isBean(Class clazz) { + return hasSetter(clazz); + } + + /** + * 判断是否有Setter方法
+ * 判定方法是是否存在只有一个参数的setXXX方法 + * + * @param clazz 待测试类 + * @return 是否为Bean对象 + * @since 4.2.2 + */ + public static boolean hasSetter(Class clazz) { + if (ClassUtil.isNormalClass(clazz)) { + final Method[] methods = clazz.getMethods(); + for (Method method : methods) { + if (method.getParameterTypes().length == 1 && method.getName().startsWith("set")) { + // 检测包含标准的setXXX方法即视为标准的JavaBean + return true; + } + } + } + return false; + } + + /** + * 判断是否为Bean对象
+ * 判定方法是是否存在只有一个参数的setXXX方法 + * + * @param clazz 待测试类 + * @return 是否为Bean对象 + * @since 4.2.2 + */ + public static boolean hasGetter(Class clazz) { + if (ClassUtil.isNormalClass(clazz)) { + final Method[] methods = clazz.getMethods(); + for (Method method : methods) { + if (method.getParameterTypes().length == 0) { + if(method.getName().startsWith("get") || method.getName().startsWith("is")) { + 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 PropertyEditor} + * + * @param type 需要转换的目标类型 + * @return {@link PropertyEditor} + */ + public static PropertyEditor findEditor(Class type) { + return PropertyEditorManager.findEditor(type); + } + + /** + * 判断Bean中是否有值为null的字段 + * + * @param bean Bean + * @return 是否有值为null的字段 + * @deprecated 请使用{@link #hasNullField(Object)} + */ + @Deprecated + public static boolean hasNull(Object bean) { + final Field[] fields = ClassUtil.getDeclaredFields(bean.getClass()); + + Object fieldValue = null; + for (Field field : fields) { + field.setAccessible(true); + try { + fieldValue = field.get(bean); + } catch (Exception e) { + //ignore + } + if (null == fieldValue) { + return true; + } + } + return false; + } + + /** + * 获取{@link BeanDesc} Bean描述信息 + * + * @param clazz Bean类 + * @return {@link BeanDesc} + * @since 3.1.2 + */ + public static BeanDesc getBeanDesc(Class clazz) { + BeanDesc beanDesc = BeanDescCache.INSTANCE.getBeanDesc(clazz); + if (null == beanDesc) { + beanDesc = new BeanDesc(clazz); + BeanDescCache.INSTANCE.putBeanDesc(clazz, beanDesc); + } + return beanDesc; + } + + // --------------------------------------------------------------------------------------------------------- PropertyDescriptor + /** + * 获得Bean字段描述数组 + * + * @param clazz Bean类 + * @return 字段描述数组 + * @throws BeanException 获取属性异常 + */ + public static PropertyDescriptor[] getPropertyDescriptors(Class clazz) throws BeanException { + BeanInfo beanInfo; + try { + beanInfo = Introspector.getBeanInfo(clazz); + } catch (IntrospectionException e) { + throw new BeanException(e); + } + return ArrayUtil.filter(beanInfo.getPropertyDescriptors(), new Filter() { + @Override + public boolean accept(PropertyDescriptor t) { + // 过滤掉getClass方法 + return false == "class".equals(t.getName()); + } + }); + } + + /** + * 获得字段名和字段描述Map,获得的结果会缓存在 {@link BeanInfoCache}中 + * + * @param clazz Bean类 + * @param ignoreCase 是否忽略大小写 + * @return 字段名和字段描述Map + * @throws BeanException 获取属性异常 + */ + public static Map getPropertyDescriptorMap(Class clazz, boolean ignoreCase) throws BeanException { + Map map = BeanInfoCache.INSTANCE.getPropertyDescriptorMap(clazz, ignoreCase); + if (null == map) { + map = internalGetPropertyDescriptorMap(clazz, ignoreCase); + BeanInfoCache.INSTANCE.putPropertyDescriptorMap(clazz, map, ignoreCase); + } + return map; + } + + /** + * 获得字段名和字段描述Map。内部使用,直接获取Bean类的PropertyDescriptor + * + * @param clazz Bean类 + * @param ignoreCase 是否忽略大小写 + * @return 字段名和字段描述Map + * @throws BeanException 获取属性异常 + */ + private static Map internalGetPropertyDescriptorMap(Class clazz, boolean ignoreCase) throws BeanException { + final PropertyDescriptor[] propertyDescriptors = getPropertyDescriptors(clazz); + final Map map = ignoreCase ? new CaseInsensitiveMap(propertyDescriptors.length, 1) + : new HashMap((int) (propertyDescriptors.length), 1); + + for (PropertyDescriptor propertyDescriptor : propertyDescriptors) { + map.put(propertyDescriptor.getName(), propertyDescriptor); + } + return map; + } + + /** + * 获得Bean类属性描述,大小写敏感 + * + * @param clazz Bean类 + * @param fieldName 字段名 + * @return PropertyDescriptor + * @throws BeanException 获取属性异常 + */ + public static PropertyDescriptor getPropertyDescriptor(Class clazz, final String fieldName) throws BeanException { + return getPropertyDescriptor(clazz, fieldName, false); + } + + /** + * 获得Bean类属性描述 + * + * @param clazz Bean类 + * @param fieldName 字段名 + * @param ignoreCase 是否忽略大小写 + * @return PropertyDescriptor + * @throws BeanException 获取属性异常 + */ + public static PropertyDescriptor getPropertyDescriptor(Class clazz, final String fieldName, boolean ignoreCase) throws BeanException { + final Map map = getPropertyDescriptorMap(clazz, ignoreCase); + return (null == map) ? null : map.get(fieldName); + } + + /** + * 获得字段值,通过反射直接获得字段值,并不调用getXXX方法
+ * 对象同样支持Map类型,fieldNameOrIndex即为key + * + * @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) { + return CollUtil.get((Collection) bean, Integer.parseInt(fieldNameOrIndex)); + } else if (ArrayUtil.isArray(bean)) { + return ArrayUtil.get(bean, Integer.parseInt(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) { + CollUtil.setOrAppend((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 bean Bean对象,支持Map、List、Collection、Array + * @param expression 表达式,例如:person.friend[5].name + * @return Bean属性值 + * @see BeanPath#get(Object) + * @since 3.0.7 + */ + public static Object getProperty(Object bean, String expression) { + return BeanPath.create(expression).get(bean); + } + + /** + * 解析Bean中的属性值 + * + * @param bean Bean对象,支持Map、List、Collection、Array + * @param expression 表达式,例如:person.friend[5].name + * @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 + */ + public static T mapToBean(Map map, Class beanClass, boolean isIgnoreError) { + return fillBeanWithMap(map, ReflectUtil.newInstance(beanClass), isIgnoreError); + } + + /** + * Map转换为Bean对象
+ * 忽略大小写 + * + * @param Bean类型 + * @param map Map + * @param beanClass Bean Class + * @param isIgnoreError 是否忽略注入错误 + * @return Bean + */ + public static T mapToBeanIgnoreCase(Map map, Class beanClass, boolean isIgnoreError) { + return fillBeanWithMapIgnoreCase(map, ReflectUtil.newInstance(beanClass), isIgnoreError); + } + + /** + * Map转换为Bean对象 + * + * @param Bean类型 + * @param map {@link Map} + * @param beanClass Bean Class + * @param copyOptions 转Bean选项 + * @return Bean + */ + public static T mapToBean(Map map, Class beanClass, CopyOptions copyOptions) { + return fillBeanWithMap(map, ReflectUtil.newInstance(beanClass), 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); + } + return BeanCopier.create(map, bean, copyOptions).copy(); + } + + // --------------------------------------------------------------------------------------------- fillBean + /** + * 对象或Map转Bean + * + * @param source Bean对象或Map + * @param clazz 目标的Bean类型 + * @return Bean对象 + * @since 4.1.20 + */ + public static T toBean(Object source, Class clazz) { + final T target = ReflectUtil.newInstance(clazz); + copyProperties(source, target); + 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) { + return fillBean(ReflectUtil.newInstance(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 + /** + * 对象转Map,不进行驼峰转下划线,不忽略值为空的字段 + * + * @param bean bean对象 + * @return Map + */ + public static Map beanToMap(Object bean) { + return beanToMap(bean, false, false); + } + + /** + * 对象转Map + * + * @param bean bean对象 + * @param isToUnderlineCase 是否转换为下划线模式 + * @param ignoreNullValue 是否忽略值为空的字段 + * @return Map + */ + public static Map beanToMap(Object bean, boolean isToUnderlineCase, boolean ignoreNullValue) { + 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 (bean == null) { + return null; + } + + return beanToMap(bean, targetMap, ignoreNullValue, new Editor() { + + @Override + public String edit(String key) { + return 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 + * @return Map + * @since 4.0.5 + */ + public static Map beanToMap(Object bean, Map targetMap, boolean ignoreNullValue, Editor keyEditor) { + if (bean == null) { + return null; + } + + final Collection props = BeanUtil.getBeanDesc(bean.getClass()).getProps(); + + String key; + Method getter; + Object value; + for (PropDesc prop : props) { + key = prop.getFieldName(); + // 过滤class属性 + // 得到property对应的getter方法 + getter = prop.getGetter(); + if (null != getter) { + // 只读取有getter方法的属性 + try { + value = getter.invoke(bean); + } catch (Exception ignore) { + continue; + } + if (false == ignoreNullValue || (null != value && false == value.equals(bean))) { + key = keyEditor.edit(key); + if (null != key) { + targetMap.put(key, value); + } + } + } + } + return targetMap; + } + + // --------------------------------------------------------------------------------------------- copyProperties + /** + * 复制Bean对象属性 + * + * @param source 源Bean对象 + * @param target 目标Bean对象 + */ + public static void copyProperties(Object source, Object target) { + copyProperties(source, target, CopyOptions.create()); + } + + /** + * 复制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 == copyOptions) { + copyOptions = new CopyOptions(); + } + BeanCopier.create(source, target, copyOptions).copy(); + } + + /** + * 给定的Bean的类名是否匹配指定类名字符串
+ * 如果isSimple为{@code false},则只匹配类名而忽略包名,例如:cn.hutool.TestEntity只匹配TestEntity
+ * 如果isSimple为{@code true},则匹配包括包名的全类名,例如: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) { + return ClassUtil.getClassName(bean, isSimple).equals(isSimple ? StrUtil.upperFirst(beanClassName) : beanClassName); + } + + /** + * 把Bean里面的String属性做trim操作。 + * + * 通常bean直接用来绑定页面的input,用户的输入可能首尾存在空格,通常保存数据库前需要把首尾空格去掉 + * + * @param Bean类型 + * @param bean Bean对象 + * @param ignoreFields 不需要trim的Field名称列表(不区分大小写) + */ + public static T trimStrFields(T bean, String... ignoreFields) { + if (bean == null) { + return bean; + } + + final Field[] fields = ReflectUtil.getFields(bean.getClass()); + for (Field field : fields) { + if (ignoreFields != null && ArrayUtil.containsIgnoreCase(ignoreFields, field.getName())) { + // 不处理忽略的Fields + continue; + } + 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 (false == val.equals(trimVal)) { + // Field Value不为null,且首尾有空格才处理 + ReflectUtil.setFieldValue(bean, field, trimVal); + } + } + } + } + + return bean; + } + + /** + * 判断Bean是否为空对象,空对象表示本身为null或者所有属性都为null + * + * @param bean Bean对象 + * @return 是否为空,true - 空 / false - 非空 + * @since 4.1.10 + */ + public static boolean isEmpty(Object bean) { + if (null != bean) { + for (Field field : ReflectUtil.getFields(bean.getClass())) { + if (null != ReflectUtil.getFieldValue(bean, field)) { + return false; + } + } + } + return true; + } + + /** + * 判断Bean是否包含值为null的属性
+ * 对象本身为null也返回true + * + * @param bean Bean对象 + * @return 是否包含值为null的属性,true - 包含 / false - 不包含 + * @since 4.1.10 + */ + public static boolean hasNullField(Object bean) { + if (null == bean) { + return true; + } + for (Field field : ReflectUtil.getFields(bean.getClass())) { + if (null == ReflectUtil.getFieldValue(bean, field)) { + return true; + } + } + return false; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/DynaBean.java b/hutool-core/src/main/java/cn/hutool/core/bean/DynaBean.java new file mode 100644 index 000000000..e106ab160 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/bean/DynaBean.java @@ -0,0 +1,195 @@ +package cn.hutool.core.bean; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Map; + +import cn.hutool.core.clone.CloneSupport; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ReflectUtil; + +/** + * 动态Bean,通过反射对Bean的相关方法做操作
+ * 支持Map和普通Bean + * + * @author Looly + * @since 3.0.7 + */ +public class DynaBean extends CloneSupport implements Serializable{ + private static final long serialVersionUID = 1L; + + private Class beanClass; + private Object bean; + + /** + * 创建一个{@link DynaBean} + * @param bean 普通Bean + * @return {@link DynaBean} + */ + public static DynaBean create(Object bean){ + return new DynaBean(bean); + } + /** + * 创建一个{@link DynaBean} + * @param beanClass Bean类 + * @param params 构造Bean所需要的参数 + * @return {@link 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 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{ + try { + final Method method = BeanUtil.getBeanDesc(beanClass).getGetter(fieldName); + if(null == method){ + throw new BeanException("No get method for {}", fieldName); + } + return (T) method.invoke(this.bean); + } catch (Exception e) { + throw new BeanException(e); + } + } + } + + /** + * 获得字段对应值,获取异常返回{@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); + return; + }else{ + try { + final Method setter = BeanUtil.getBeanDesc(beanClass).getSetter(fieldName); + if(null == setter){ + throw new BeanException("No set method for {}", fieldName); + } + setter.invoke(this.bean, value); + } catch (Exception e) { + throw new BeanException(e); + } + } + } + + /** + * 执行原始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) { + if (other.bean != null) { + return false; + } + } else if (!bean.equals(other.bean)) { + return false; + } + return true; + } + + @Override + public String toString() { + return this.bean.toString(); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanCopier.java b/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanCopier.java new file mode 100644 index 000000000..0e7735228 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanCopier.java @@ -0,0 +1,300 @@ +package cn.hutool.core.bean.copier; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; + +import cn.hutool.core.bean.BeanDesc.PropDesc; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.copier.provider.BeanValueProvider; +import cn.hutool.core.bean.copier.provider.MapValueProvider; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.lang.ParameterizedTypeImpl; +import cn.hutool.core.lang.copier.Copier; +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 cn.hutool.core.util.TypeUtil; + +/** + * Bean拷贝 + * + * @author looly + * + * @param 目标对象类型 + * @since 3.2.3 + */ +public class BeanCopier implements Copier, Serializable { + private static final long serialVersionUID = 1L; + + /** 源对象 */ + private Object source; + /** 目标对象 */ + private T dest; + /** 目标的类型(用于泛型类注入) */ + private Type destType; + /** 拷贝选项 */ + private CopyOptions copyOptions; + + /** + * 创建BeanCopier + * + * @param 目标Bean类型 + * @param source 来源对象,可以是Bean或者Map + * @param dest 目标Bean对象 + * @param copyOptions 拷贝属性选项 + * @return BeanCopier + */ + public static BeanCopier create(Object source, T dest, CopyOptions copyOptions) { + return create(source, dest, dest.getClass(), copyOptions); + } + + /** + * 创建BeanCopier + * + * @param 目标Bean类型 + * @param source 来源对象,可以是Bean或者Map + * @param dest 目标Bean对象 + * @param destType 目标的泛型类型,用于标注有泛型参数的Bean对象 + * @param copyOptions 拷贝属性选项 + * @return BeanCopier + */ + public static BeanCopier create(Object source, T dest, Type destType, CopyOptions copyOptions) { + return new BeanCopier<>(source, dest, destType, copyOptions); + } + + /** + * 构造 + * + * @param source 来源对象,可以是Bean或者Map + * @param dest 目标Bean对象 + * @param destType 目标的泛型类型,用于标注有泛型参数的Bean对象 + * @param copyOptions 拷贝属性选项 + */ + public BeanCopier(Object source, T dest, Type destType, CopyOptions copyOptions) { + this.source = source; + this.dest = dest; + this.destType = destType; + this.copyOptions = copyOptions; + } + + @Override + @SuppressWarnings("unchecked") + public T copy() { + if (null != this.source) { + if (this.source instanceof ValueProvider) { + // 目标只支持Bean + valueProviderToBean((ValueProvider) this.source, this.dest); + } else if (this.source instanceof Map) { + if (this.dest instanceof Map) { + mapToMap((Map) this.source, (Map) this.dest); + } else { + mapToBean((Map) this.source, this.dest); + } + } else { + if (this.dest instanceof Map) { + beanToMap(this.source, (Map) this.dest); + } else { + beanToBean(this.source, this.dest); + } + } + } + return this.dest; + } + + /** + * Bean和Bean之间属性拷贝 + * + * @param providerBean 来源Bean + * @param destBean 目标Bean + */ + private void beanToBean(Object providerBean, Object destBean) { + valueProviderToBean(new BeanValueProvider(providerBean, this.copyOptions.ignoreCase, this.copyOptions.ignoreError), destBean); + } + + /** + * Map转Bean属性拷贝 + * + * @param map Map + * @param bean Bean + */ + private void mapToBean(Map map, Object bean) { + valueProviderToBean(new MapValueProvider(map, this.copyOptions.ignoreCase), bean); + } + + /** + * Map转Map + * + * @param source 源Map + * @param dest 目标Map + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + private void mapToMap(Map source, Map dest) { + if (null != dest && null != source) { + dest.putAll(source); + } + } + + /** + * 对象转Map + * + * @param bean bean对象 + * @param targetMap 目标的Map + * @since 4.1.22 + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + private void beanToMap(Object bean, Map targetMap) { + final Collection props = BeanUtil.getBeanDesc(bean.getClass()).getProps(); + final HashSet ignoreSet = (null != copyOptions.ignoreProperties) ? CollUtil.newHashSet(copyOptions.ignoreProperties) : null; + final CopyOptions copyOptions = this.copyOptions; + + String key; + Method getter; + Object value; + for (PropDesc prop : props) { + key = prop.getFieldName(); + // 过滤class属性 + // 得到property对应的getter方法 + getter = prop.getGetter(); + if (null != getter) { + // 只读取有getter方法的属性 + try { + value = getter.invoke(bean); + } catch (Exception e) { + if (copyOptions.ignoreError) { + continue;// 忽略反射失败 + } else { + throw new UtilException(e, "Get value of [{}] error!", prop.getFieldName()); + } + } + if (CollUtil.contains(ignoreSet, key)) { + // 目标属性值被忽略或值提供者无此key时跳过 + continue; + } + if (null == value && copyOptions.ignoreNullValue) { + continue;// 当允许跳过空时,跳过 + } + if (bean.equals(value)) { + continue;// 值不能为bean本身,防止循环引用 + } + targetMap.put(mappingKey(copyOptions.fieldMapping, key), value); + } + } + } + + /** + * 值提供器转Bean + * + * @param valueProvider 值提供器 + * @param bean Bean + */ + private void valueProviderToBean(ValueProvider valueProvider, Object bean) { + if (null == valueProvider) { + return; + } + + final CopyOptions copyOptions = this.copyOptions; + Class actualEditable = bean.getClass(); + if (null != copyOptions.editable) { + // 检查限制类是否为target的父类或接口 + if (false == copyOptions.editable.isInstance(bean)) { + throw new IllegalArgumentException(StrUtil.format("Target class [{}] not assignable to Editable class [{}]", bean.getClass().getName(), copyOptions.editable.getName())); + } + actualEditable = copyOptions.editable; + } + final HashSet ignoreSet = (null != copyOptions.ignoreProperties) ? CollUtil.newHashSet(copyOptions.ignoreProperties) : null; + final Map fieldReverseMapping = copyOptions.getReversedMapping(); + + final Collection props = BeanUtil.getBeanDesc(actualEditable).getProps(); + String fieldName; + Object value; + Method setterMethod; + Class propClass; + for (PropDesc prop : props) { + // 获取值 + fieldName = prop.getFieldName(); + if (CollUtil.contains(ignoreSet, fieldName)) { + // 目标属性值被忽略或值提供者无此key时跳过 + continue; + } + final String providerKey = mappingKey(fieldReverseMapping, fieldName); + if (false == valueProvider.containsKey(providerKey)) { + // 无对应值可提供 + continue; + } + setterMethod = prop.getSetter(); + if (null == setterMethod) { + // Setter方法不存在跳过 + continue; + } + + Type firstParamType = TypeUtil.getFirstParamType(setterMethod); + if (firstParamType instanceof ParameterizedType) { + // 参数为泛型参数类型,解析对应泛型类型为真实类型 + ParameterizedType tmp = (ParameterizedType) firstParamType; + Type[] actualTypeArguments = tmp.getActualTypeArguments(); + if (TypeUtil.hasTypeVeriable(actualTypeArguments)) { + // 泛型对象中含有未被转换的泛型变量 + actualTypeArguments = TypeUtil.getActualTypes(this.destType, setterMethod.getDeclaringClass(), tmp.getActualTypeArguments()); + if (ArrayUtil.isNotEmpty(actualTypeArguments)) { + // 替换泛型变量为实际类型 + firstParamType = new ParameterizedTypeImpl(actualTypeArguments, tmp.getOwnerType(), tmp.getRawType()); + } + } + } else if (firstParamType instanceof TypeVariable) { + // 参数为泛型,查找其真实类型(适用于泛型方法定义于泛型父类) + firstParamType = TypeUtil.getActualType(this.destType, setterMethod.getDeclaringClass(), firstParamType); + } + + value = valueProvider.value(providerKey, firstParamType); + if (null == value && copyOptions.ignoreNullValue) { + continue;// 当允许跳过空时,跳过 + } + if (bean.equals(value)) { + continue;// 值不能为bean本身,防止循环引用 + } + + try { + // valueProvider在没有对值做转换且当类型不匹配的时候,执行默认转换 + propClass = prop.getFieldClass(); + if (false ==propClass.isInstance(value)) { + value = Convert.convert(propClass, value); + if (null == value && copyOptions.ignoreNullValue) { + continue;// 当允许跳过空时,跳过 + } + } + + // 执行set方法注入值 + setterMethod.invoke(bean, value); + } catch (Exception e) { + if (false ==copyOptions.ignoreError) { + throw new UtilException(e, "Inject [{}] error!", prop.getFieldName()); + } + // 忽略注入失败 + } + } + } + + /** + * 获取指定字段名对应的映射值 + * + * @param mapping 反向映射Map + * @param fieldName 字段名 + * @return 映射值,无对应值返回字段名 + * @since 4.1.10 + */ + private static String mappingKey(Map mapping, String fieldName) { + if (MapUtil.isEmpty(mapping)) { + return fieldName; + } + return ObjectUtil.defaultIfNull(mapping.get(fieldName), fieldName); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/copier/CopyOptions.java b/hutool-core/src/main/java/cn/hutool/core/bean/copier/CopyOptions.java new file mode 100644 index 000000000..7ffebd75c --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/bean/copier/CopyOptions.java @@ -0,0 +1,177 @@ +package cn.hutool.core.bean.copier; + +import java.io.Serializable; +import java.util.Map; + +import cn.hutool.core.map.MapUtil; + +/** + * 属性拷贝选项
+ * 包括:
+ * 1、限制的类或接口,必须为目标对象的实现接口或父类,用于限制拷贝的属性,例如一个类我只想复制其父类的一些属性,就可以将editable设置为父类
+ * 2、是否忽略空值,当源对象的值为null时,true: 忽略而不注入此值,false: 注入null
+ * 3、忽略的属性列表,设置一个属性列表,不拷贝这些属性值
+ * + * @author Looly + */ +public class CopyOptions implements Serializable{ + private static final long serialVersionUID = 1L; + + /** 限制的类或接口,必须为目标对象的实现接口或父类,用于限制拷贝的属性,例如一个类我只想复制其父类的一些属性,就可以将editable设置为父类 */ + protected Class editable; + /** 是否忽略空值,当源对象的值为null时,true: 忽略而不注入此值,false: 注入null */ + protected boolean ignoreNullValue; + /** 忽略的目标对象中属性列表,设置一个属性列表,不拷贝这些属性值 */ + protected String[] ignoreProperties; + /** 是否忽略字段注入错误 */ + protected boolean ignoreError; + /** 是否忽略字段大小写 */ + protected boolean ignoreCase; + /** 拷贝属性的字段映射,用于不同的属性之前拷贝做对应表用 */ + protected Map fieldMapping; + + /** + * 创建拷贝选项 + * + * @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); + } + + /** + * 构造拷贝选项 + */ + public CopyOptions() { + } + + /** + * 构造拷贝选项 + * + * @param editable 限制的类或接口,必须为目标对象的实现接口或父类,用于限制拷贝的属性 + * @param ignoreNullValue 是否忽略空值,当源对象的值为null时,true: 忽略而不注入此值,false: 注入null + * @param ignoreProperties 忽略的目标对象中属性列表,设置一个属性列表,不拷贝这些属性值 + */ + public CopyOptions(Class editable, boolean ignoreNullValue, String... ignoreProperties) { + this.editable = editable; + this.ignoreNullValue = ignoreNullValue; + this.ignoreProperties = 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); + } + + /** + * 设置忽略的目标对象中属性列表,设置一个属性列表,不拷贝这些属性值 + * + * @param ignoreProperties 忽略的目标对象中属性列表,设置一个属性列表,不拷贝这些属性值 + * @return CopyOptions + */ + public CopyOptions setIgnoreProperties(String... ignoreProperties) { + this.ignoreProperties = ignoreProperties; + 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); + } + + /** + * 设置拷贝属性的字段映射,用于不同的属性之前拷贝做对应表用 + * + * @param fieldMapping 拷贝属性的字段映射,用于不同的属性之前拷贝做对应表用 + * @return CopyOptions + */ + public CopyOptions setFieldMapping(Map fieldMapping) { + this.fieldMapping = fieldMapping; + return this; + } + + /** + * 获取反转之后的映射 + * @return 反转映射 + * @since 4.1.10 + */ + protected Map getReversedMapping() { + return (null != this.fieldMapping) ? MapUtil.reverse(this.fieldMapping) : null; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/copier/ValueProvider.java b/hutool-core/src/main/java/cn/hutool/core/bean/copier/ValueProvider.java new file mode 100644 index 000000000..f907472a6 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/bean/copier/ValueProvider.java @@ -0,0 +1,36 @@ +package cn.hutool.core.bean.copier; + +import java.lang.reflect.Type; + +import cn.hutool.core.convert.Convert; + +/** + * 值提供者,用于提供Bean注入时参数对应值得抽象接口
+ * 继承或匿名实例化此接口
+ * 在Bean注入过程中,Bean获得字段名,通过外部方式根据这个字段名查找相应的字段值,然后注入Bean
+ * + * @author Looly + * @param KEY类型,一般情况下为 {@link String} + * + */ +public interface ValueProvider{ + + /** + * 获取值
+ * 返回值一般需要匹配被注入类型,如果不匹配会调用默认转换 {@link Convert#convert(Type, Object)}实现转换 + * + * @param key Bean对象中参数名 + * @param valueType 被注入的值得类型 + * @return 对应参数名的值 + */ + public Object value(T key, Type valueType); + + /** + * 是否包含指定KEY,如果不包含则忽略注入
+ * 此接口方法单独需要实现的意义在于:有些值提供者(比如Map)key是存在的,但是value为null,此时如果需要注入这个null,需要根据此方法判断 + * + * @param key Bean对象中参数名 + * @return 是否包含指定KEY + */ + public boolean containsKey(T key); +} diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/copier/package-info.java b/hutool-core/src/main/java/cn/hutool/core/bean/copier/package-info.java new file mode 100644 index 000000000..54f1c25b0 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/bean/copier/provider/BeanValueProvider.java b/hutool-core/src/main/java/cn/hutool/core/bean/copier/provider/BeanValueProvider.java new file mode 100644 index 000000000..1a464e7f0 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/bean/copier/provider/BeanValueProvider.java @@ -0,0 +1,66 @@ +package cn.hutool.core.bean.copier.provider; + +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.Map; + +import cn.hutool.core.bean.BeanDesc.PropDesc; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.copier.ValueProvider; +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.util.StrUtil; + +/** + * Bean的值提供者 + * + * @author looly + * + */ +public class BeanValueProvider implements ValueProvider { + + private Object source; + private boolean ignoreError; + final Map sourcePdMap; + + /** + * 构造 + * + * @param bean Bean + * @param ignoreCase 是否忽略字段大小写 + * @param ignoreError 是否忽略字段值读取错误 + */ + public BeanValueProvider(Object bean, boolean ignoreCase, boolean ignoreError) { + this.source = bean; + this.ignoreError = ignoreError; + sourcePdMap = BeanUtil.getBeanDesc(source.getClass()).getPropMap(ignoreCase); + } + + @Override + public Object value(String key, Type valueType) { + PropDesc sourcePd = sourcePdMap.get(key); + if(null == sourcePd && (Boolean.class == valueType || boolean.class == valueType)) { + //boolean类型字段字段名支持两种方式 + sourcePd = sourcePdMap.get(StrUtil.upperFirstAndAddPre(key, "is")); + } + + if (null != sourcePd) { + final Method getter = sourcePd.getGetter(); + if (null != getter) { + try { + return getter.invoke(source); + } catch (Exception e) { + if (false == ignoreError) { + throw new UtilException(e, "Inject [{}] error!", key); + } + } + } + } + return null; + } + + @Override + public boolean containsKey(String key) { + return sourcePdMap.containsKey(key) || sourcePdMap.containsKey(StrUtil.upperFirstAndAddPre(key, "is")); + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/copier/provider/MapValueProvider.java b/hutool-core/src/main/java/cn/hutool/core/bean/copier/provider/MapValueProvider.java new file mode 100644 index 000000000..4a9c74b6f --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/bean/copier/provider/MapValueProvider.java @@ -0,0 +1,59 @@ +package cn.hutool.core.bean.copier.provider; + +import java.lang.reflect.Type; +import java.util.Map; + +import cn.hutool.core.bean.copier.ValueProvider; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.map.CaseInsensitiveMap; +import cn.hutool.core.util.StrUtil; + +/** + * Map值提供者 + * + * @author looly + * + */ +public class MapValueProvider implements ValueProvider { + + private Map map; + + /** + * 构造 + * + * @param map Map + * @param ignoreCase 是否忽略key的大小写 + */ + public MapValueProvider(Map map, boolean ignoreCase) { + if(false == ignoreCase || map instanceof CaseInsensitiveMap) { + //不忽略大小写或者提供的Map本身为CaseInsensitiveMap则无需转换 + this.map = map; + }else { + //转换为大小写不敏感的Map + this.map = new CaseInsensitiveMap<>(map); + } + } + + @Override + public Object value(String key, Type valueType) { + Object value = map.get(key); + if(null == value) { + //检查下划线模式 + value = map.get(StrUtil.toUnderlineCase(key)); + } + + return Convert.convert(valueType, value); + } + + @Override + public boolean containsKey(String key) { + if(map.containsKey(key)) { + return true; + }else if(map.containsKey(StrUtil.toUnderlineCase(key))) { + //检查下划线模式 + return true; + } + return false; + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/copier/provider/package-info.java b/hutool-core/src/main/java/cn/hutool/core/bean/copier/provider/package-info.java new file mode 100644 index 000000000..1014fe7cc --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/bean/package-info.java b/hutool-core/src/main/java/cn/hutool/core/bean/package-info.java new file mode 100644 index 000000000..01479a9fe --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/builder/Builder.java b/hutool-core/src/main/java/cn/hutool/core/builder/Builder.java new file mode 100644 index 000000000..05cef79e0 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/builder/CompareToBuilder.java b/hutool-core/src/main/java/cn/hutool/core/builder/CompareToBuilder.java new file mode 100644 index 000000000..77cf72479 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/builder/CompareToBuilder.java @@ -0,0 +1,976 @@ +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.Comparator; + +import cn.hutool.core.util.ArrayUtil; + +/** + * 用于构建 {@link java.lang.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() { + super(); + 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 (false == 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 = ((lhs < rhs) ? -1 : ((lhs > rhs) ? 1 : 0)); + 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 = ((lhs < rhs) ? -1 : ((lhs > rhs) ? 1 : 0)); + 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 = ((lhs < rhs) ? -1 : ((lhs > rhs) ? 1 : 0)); + 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 = ((lhs < rhs) ? -1 : ((lhs > rhs) ? 1 : 0)); + 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 = ((lhs < rhs) ? -1 : ((lhs > rhs) ? 1 : 0)); + 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 == false) { + 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/hutool-core/src/main/java/cn/hutool/core/builder/EqualsBuilder.java b/hutool-core/src/main/java/cn/hutool/core/builder/EqualsBuilder.java new file mode 100644 index 000000000..d3ab0f42c --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/builder/EqualsBuilder.java @@ -0,0 +1,878 @@ +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.Pair; +import cn.hutool.core.util.ArrayUtil; + +/** + *

{@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);
+ * }
+ * 
+ * + */ +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. + *

+ * + * @since 3.0 + */ + 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 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 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 this object to lookup in registry + * @param rhs the other object to lookup on registry + * @return boolean 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 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 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(); + } + } + } + } + + /** + * If the fields tested are equals. + * The default value is true. + */ + /** 是否equals,此值随着构建会变更,默认true */ + private boolean isEquals = true; + + /** + *

Constructor for EqualsBuilder.

+ * + *

Starts off assuming that equals is true.

+ * @see Object#equals(Object) + */ + /** + * 构造,初始状态值为true + */ + public EqualsBuilder() { + // do nothing for now. + } + + //------------------------------------------------------------------------- + + /** + *

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

+ * + * @param lhs 此对象 + * @param rhs 另一个对象 + * @param excludeFields 排除的字段集合,如果有不参与计算equals的字段加入此集合即可 + * @return 两个对象是否equals,是返回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,是返回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 Objects + * are equal.

+ * + *

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. Non-primitive fields are compared using + * equals().

+ * + *

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.

+ * + * @param lhs this object + * @param rhs the other object + * @param testTransients whether to include transient fields + * @return 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 Objects + * are equal.

+ * + *

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. Non-primitive fields are compared using + * equals().

+ * + *

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 appended + * up to and including the specified superclass. A null superclass is treated + * as java.lang.Object.

+ * + * @param lhs 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 null + * @param excludeFields array of field names to exclude from testing + * @return 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 (false == 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 super.equals() to this builder.

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

Test if two Objects are equal using their + * 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 == false) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null || rhs == null) { + this.setEquals(false); + return this; + } + final Class lhsClass = lhs.getClass(); + if (!lhsClass.isArray()) { + // The simple case, not an array, just test the element + isEquals = lhs.equals(rhs); + } else if (lhs.getClass() != rhs.getClass()) { + // Here when we compare different dimensions, for example: a boolean[][] to a boolean[] + this.setEquals(false); + } + // 'Switch' on type of array, to dispatch to the correct handler + // This handles multi dimensional arrays of the same depth + else 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 + append((Object[]) lhs, (Object[]) rhs); + } + return this; + } + + /** + *

+ * Test if two long s are equal. + *

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

Test if two ints are equal.

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

Test if two shorts are equal.

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

Test if two chars are equal.

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

Test if two bytes are equal.

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

Test if two doubles are equal by testing that the + * pattern of bits returned by doubleToLong are equal.

+ * + *

This handles NaNs, Infinities, and -0.0.

+ * + *

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

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

Test if two floats are equal byt testing that the + * pattern of bits returned by doubleToLong are equal.

+ * + *

This handles NaNs, Infinities, and -0.0.

+ * + *

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

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

Test if two booleanss are equal.

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

Performs a deep comparison of two Object arrays.

+ * + *

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

+ * + * @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 == false) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null || rhs == null) { + this.setEquals(false); + return this; + } + if (lhs.length != rhs.length) { + this.setEquals(false); + return this; + } + for (int i = 0; i < lhs.length && isEquals; ++i) { + append(lhs[i], rhs[i]); + } + return this; + } + + /** + *

Deep comparison of array of long. Length and all + * values are compared.

+ * + *

The method {@link #append(long, long)} is used.

+ * + * @param lhs the left hand long[] + * @param rhs the right hand long[] + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(final long[] lhs, final long[] rhs) { + if (isEquals == false) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null || rhs == null) { + this.setEquals(false); + return this; + } + if (lhs.length != rhs.length) { + this.setEquals(false); + return this; + } + for (int i = 0; i < lhs.length && isEquals; ++i) { + append(lhs[i], rhs[i]); + } + return this; + } + + /** + *

Deep comparison of array of int. Length and all + * values are compared.

+ * + *

The method {@link #append(int, int)} is used.

+ * + * @param lhs the left hand int[] + * @param rhs the right hand int[] + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(final int[] lhs, final int[] rhs) { + if (isEquals == false) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null || rhs == null) { + this.setEquals(false); + return this; + } + if (lhs.length != rhs.length) { + this.setEquals(false); + return this; + } + for (int i = 0; i < lhs.length && isEquals; ++i) { + append(lhs[i], rhs[i]); + } + return this; + } + + /** + *

Deep comparison of array of short. Length and all + * values are compared.

+ * + *

The method {@link #append(short, short)} is used.

+ * + * @param lhs the left hand short[] + * @param rhs the right hand short[] + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(final short[] lhs, final short[] rhs) { + if (isEquals == false) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null || rhs == null) { + this.setEquals(false); + return this; + } + if (lhs.length != rhs.length) { + this.setEquals(false); + return this; + } + for (int i = 0; i < lhs.length && isEquals; ++i) { + append(lhs[i], rhs[i]); + } + return this; + } + + /** + *

Deep comparison of array of char. Length and all + * values are compared.

+ * + *

The method {@link #append(char, char)} is used.

+ * + * @param lhs the left hand char[] + * @param rhs the right hand char[] + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(final char[] lhs, final char[] rhs) { + if (isEquals == false) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null || rhs == null) { + this.setEquals(false); + return this; + } + if (lhs.length != rhs.length) { + this.setEquals(false); + return this; + } + for (int i = 0; i < lhs.length && isEquals; ++i) { + append(lhs[i], rhs[i]); + } + return this; + } + + /** + *

Deep comparison of array of byte. Length and all + * values are compared.

+ * + *

The method {@link #append(byte, byte)} is used.

+ * + * @param lhs the left hand byte[] + * @param rhs the right hand byte[] + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(final byte[] lhs, final byte[] rhs) { + if (isEquals == false) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null || rhs == null) { + this.setEquals(false); + return this; + } + if (lhs.length != rhs.length) { + this.setEquals(false); + return this; + } + for (int i = 0; i < lhs.length && isEquals; ++i) { + append(lhs[i], rhs[i]); + } + return this; + } + + /** + *

Deep comparison of array of double. Length and all + * values are compared.

+ * + *

The method {@link #append(double, double)} is used.

+ * + * @param lhs the left hand double[] + * @param rhs the right hand double[] + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(final double[] lhs, final double[] rhs) { + if (isEquals == false) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null || rhs == null) { + this.setEquals(false); + return this; + } + if (lhs.length != rhs.length) { + this.setEquals(false); + return this; + } + for (int i = 0; i < lhs.length && isEquals; ++i) { + append(lhs[i], rhs[i]); + } + return this; + } + + /** + *

Deep comparison of array of float. Length and all + * values are compared.

+ * + *

The method {@link #append(float, float)} is used.

+ * + * @param lhs the left hand float[] + * @param rhs the right hand float[] + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(final float[] lhs, final float[] rhs) { + if (isEquals == false) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null || rhs == null) { + this.setEquals(false); + return this; + } + if (lhs.length != rhs.length) { + this.setEquals(false); + return this; + } + for (int i = 0; i < lhs.length && isEquals; ++i) { + append(lhs[i], rhs[i]); + } + return this; + } + + /** + *

Deep comparison of array of boolean. Length and all + * values are compared.

+ * + *

The method {@link #append(boolean, boolean)} is used.

+ * + * @param lhs the left hand boolean[] + * @param rhs the right hand boolean[] + * @return EqualsBuilder - used to chain calls. + */ + public EqualsBuilder append(final boolean[] lhs, final boolean[] rhs) { + if (isEquals == false) { + return this; + } + if (lhs == rhs) { + return this; + } + if (lhs == null || rhs == null) { + this.setEquals(false); + return this; + } + if (lhs.length != rhs.length) { + this.setEquals(false); + return this; + } + for (int i = 0; i < lhs.length && isEquals; ++i) { + append(lhs[i], rhs[i]); + } + return this; + } + + /** + *

Returns true if the fields that have been checked + * are all equal.

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

Returns true if the fields that have been checked + * are all equal.

+ * + * @return true if all of the fields that have been checked + * are equal, false otherwise. + * + * @since 3.0 + */ + @Override + public Boolean build() { + return Boolean.valueOf(isEquals()); + } + + /** + * Sets the isEquals value. + * + * @param isEquals The value to set. + * @since 2.1 + */ + protected void setEquals(final boolean isEquals) { + this.isEquals = isEquals; + } + + /** + * Reset the EqualsBuilder so you can use the same object again + * @since 2.5 + */ + public void reset() { + this.isEquals = true; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/builder/HashCodeBuilder.java b/hutool-core/src/main/java/cn/hutool/core/builder/HashCodeBuilder.java new file mode 100644 index 000000000..ad95336ca --- /dev/null +++ b/hutool-core/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 (false == 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 = 0; + + /** + *

+ * 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 Integer.valueOf(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/hutool-core/src/main/java/cn/hutool/core/builder/IDKey.java b/hutool-core/src/main/java/cn/hutool/core/builder/IDKey.java new file mode 100644 index 000000000..af0c07fad --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/builder/IDKey.java @@ -0,0 +1,63 @@ +package cn.hutool.core.builder; + +import java.io.Serializable; + +/** + * Wrap an identity key (System.identityHashCode()) so that an object can only be equal() to itself. + * + * This is necessary to disambiguate the occasional duplicate identityHashCodes that can occur. + * + * TODO 待整理 + * 来自于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; + + /** + * Constructor for IDKey + * + * @param _value The value + */ + public IDKey(final Object _value) { + // This is the Object hashcode + id = System.identityHashCode(_value); + // 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 = _value; + } + + /** + * 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/hutool-core/src/main/java/cn/hutool/core/builder/package-info.java b/hutool-core/src/main/java/cn/hutool/core/builder/package-info.java new file mode 100644 index 000000000..7f89a28b6 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/clone/CloneRuntimeException.java b/hutool-core/src/main/java/cn/hutool/core/clone/CloneRuntimeException.java new file mode 100644 index 000000000..b0b7bd08a --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/clone/CloneSupport.java b/hutool-core/src/main/java/cn/hutool/core/clone/CloneSupport.java new file mode 100644 index 000000000..177b621b8 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/clone/Cloneable.java b/hutool-core/src/main/java/cn/hutool/core/clone/Cloneable.java new file mode 100644 index 000000000..4b26c0486 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/clone/package-info.java b/hutool-core/src/main/java/cn/hutool/core/clone/package-info.java new file mode 100644 index 000000000..f607b1de1 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/codec/BCD.java b/hutool-core/src/main/java/cn/hutool/core/codec/BCD.java new file mode 100644 index 000000000..b0762b256 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/codec/BCD.java @@ -0,0 +1,119 @@ +package cn.hutool.core.codec; + +/** + * BCD码(Binary-Coded Decimal‎)亦称二进码十进数或二-十进制代码
+ * BCD码这种编码形式利用了四个位元来储存一个十进制的数码,使二进制和十进制之间的转换得以快捷的进行
+ * see http://cuisuqiang.iteye.com/blog/1429956 + * @author Looly + * + */ +public class BCD { + + /** + * 字符串转BCD码 + * @param asc ASCII字符串 + * @return BCD + */ + public static byte[] strToBcd(String asc) { + int len = asc.length(); + int mod = len % 2; + if (mod != 0) { + asc = "0" + asc; + len = asc.length(); + } + byte abt[] = new byte[len]; + if (len >= 2) { + len >>= 1; + } + byte 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) { + return ascToBcd(ascii, ascii.length); + } + + /** + * ASCII转BCD + * @param ascii ASCII byte数组 + * @param ascLength 长度 + * @return BCD + */ + public static byte[] ascToBcd(byte[] ascii, int ascLength) { + 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) { + char temp[] = new char[bytes.length * 2], 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/hutool-core/src/main/java/cn/hutool/core/codec/Base32.java b/hutool-core/src/main/java/cn/hutool/core/codec/Base32.java new file mode 100644 index 000000000..4b037539d --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/codec/Base32.java @@ -0,0 +1,187 @@ +package cn.hutool.core.codec; + +import java.nio.charset.Charset; + +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; + +/** + * Base32 - encodes and decodes RFC3548 Base32 (see http://www.faqs.org/rfcs/rfc3548.html )
+ * base32就是用32(2的5次方)个特定ASCII码来表示256个ASCII码。
+ * 所以,5个ASCII字符经过base32编码后会变为8个字符(公约数为40),长度增加3/5.不足8n用“=”补足。 + * see http://blog.csdn.net/earbao/article/details/44453937 + * @author Looly + * + */ +public class Base32 { + + private Base32() {} + + private static final String base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + private static final int[] base32Lookup = {// + 0xFF, 0xFF, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, // '0', '1', '2', '3', '4', '5', '6', '7' + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // '8', '9', ':', ';', '<', '=', '>', '?' + 0xFF, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, // '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G' + 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, // 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O' + 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, // 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W' + 0x17, 0x18, 0x19, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 'X', 'Y', 'Z', '[', '\', ']', '^', '_' + 0xFF, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, // '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g' + 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, // 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o' + 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, // 'p', 'q', 'r', 's', 't', 'u', 'v', 'w' + 0x17, 0x18, 0x19, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF // 'x', 'y', 'z', '{', '|', '}', '~', 'DEL' + }; + + //----------------------------------------------------------------------------------------- encode + /** + * 编码 + * @param bytes 数据 + * @return base32 + */ + public static String encode(final byte[] bytes) { + int i = 0, index = 0, digit = 0; + int currByte, nextByte; + StringBuilder base32 = new StringBuilder((bytes.length + 7) * 8 / 5); + + while (i < bytes.length) { + currByte = (bytes[i] >= 0) ? bytes[i] : (bytes[i] + 256); // unsign + + /* Is the current digit going to span a byte boundary? */ + if (index > 3) { + if ((i + 1) < bytes.length) { + nextByte = (bytes[i + 1] >= 0) ? bytes[i + 1] : (bytes[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(base32Chars.charAt(digit)); + } + + return base32.toString(); + } + + /** + * 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, String charset) { + return encode(StrUtil.bytes(source, charset)); + } + + /** + * base32编码 + * + * @param source 被编码的base32字符串 + * @param charset 字符集 + * @return 被加密后的字符串 + */ + public static String encode(String source, Charset charset) { + return encode(StrUtil.bytes(source, charset)); + } + + //----------------------------------------------------------------------------------------- decode + /** + * 解码 + * @param base32 base32编码 + * @return 数据 + */ + public static byte[] decode(final String base32) { + int i, index, lookup, offset, digit; + byte[] bytes = new byte[base32.length() * 5 / 8]; + + for (i = 0, index = 0, offset = 0; i < base32.length(); i++) { + lookup = base32.charAt(i) - '0'; + + /* Skip chars outside the lookup table */ + if (lookup < 0 || lookup >= base32Lookup.length) { + continue; + } + + digit = base32Lookup[lookup]; + + /* If this digit is not in the table, ignore it */ + if (digit == 0xFF) { + 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; + } + + /** + * 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, String charset) { + return StrUtil.str(decode(source), charset); + } + + /** + * base32解码 + * + * @param source 被解码的base32字符串 + * @param charset 字符集 + * @return 被加密后的字符串 + */ + public static String decodeStr(String source, Charset charset) { + return StrUtil.str(decode(source), charset); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/codec/Base62.java b/hutool-core/src/main/java/cn/hutool/core/codec/Base62.java new file mode 100644 index 000000000..98075bc14 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/codec/Base62.java @@ -0,0 +1,149 @@ +package cn.hutool.core.codec; + +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; + +/** + * Base62工具类,提供Base62的编码和解码方案
+ * + * @author Looly + * @since 4.5.9 + */ +public class Base62 { + + private static final Charset DEFAULT_CHARSET = CharsetUtil.CHARSET_UTF_8; + private static final Base62Codec codec = Base62Codec.createGmp(); + + // -------------------------------------------------------------------- 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(codec.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)); + } + + // -------------------------------------------------------------------- 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 codec.decode(base62bytes); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/codec/Base62Codec.java b/hutool-core/src/main/java/cn/hutool/core/codec/Base62Codec.java new file mode 100644 index 000000000..c945322d0 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/codec/Base62Codec.java @@ -0,0 +1,180 @@ +package cn.hutool.core.codec; + +import java.io.ByteArrayOutputStream; +import java.io.Serializable; + +import cn.hutool.core.util.ArrayUtil; + +/** + * Base62编码解码实现,常用于短URL
+ * From https://github.com/seruco/base62 + * + * @author Looly, Sebastian Ruhleder, sebastian@seruco.io + * @since 4.5.9 + */ +public class Base62Codec implements Serializable{ + private static final long serialVersionUID = 1L; + + private static final int STANDARD_BASE = 256; + private static final int TARGET_BASE = 62; + + /** + * 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' // + }; + + /** + * 创建GMP风格的Base62编码解码器对象 + * + * @return {@link Base62Codec} + */ + public static Base62Codec createGmp() { + return new Base62Codec(GMP); + } + + /** + * 创建Inverted风格的Base62编码解码器对象 + * + * @return {@link Base62Codec} + */ + public static Base62Codec createInverted() { + return new Base62Codec(INVERTED); + } + + private final byte[] alphabet; + private final byte[] lookup; + + /** + * 构造 + * + * @param alphabet 自定义字母表 + */ + public Base62Codec(byte[] alphabet) { + this.alphabet = alphabet; + lookup = new byte[256]; + for (int i = 0; i < alphabet.length; i++) { + lookup[alphabet[i]] = (byte) (i & 0xFF); + } + } + + /** + * 编码指定消息bytes为Base62格式的bytes + * + * @param message 被编码的消息 + * @return Base62内容 + */ + public byte[] encode(byte[] message) { + final byte[] indices = convert(message, STANDARD_BASE, TARGET_BASE); + return translate(indices, alphabet); + } + + /** + * 解码Base62消息 + * + * @param encoded Base62内容 + * @return 消息 + */ + public byte[] decode(byte[] encoded) { + final byte[] prepared = translate(encoded, lookup); + return convert(prepared, TARGET_BASE, STANDARD_BASE); + } + + // --------------------------------------------------------------------------------------------------------------- Private method start + /** + * 按照字典转换bytes + * + * @param indices 内容 + * @param dictionary 字典 + * @return 转换值 + */ + private 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 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 (int i = 0; i < source.length; i++) { + final int accumulator = (source[i] & 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 int estimateOutputLength(int inputLength, int sourceBase, int targetBase) { + return (int) Math.ceil((Math.log(sourceBase) / Math.log(targetBase)) * inputLength); + } + // --------------------------------------------------------------------------------------------------------------- Private method end +} \ No newline at end of file diff --git a/hutool-core/src/main/java/cn/hutool/core/codec/Base64.java b/hutool-core/src/main/java/cn/hutool/core/codec/Base64.java new file mode 100644 index 000000000..d796f882b --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/codec/Base64.java @@ -0,0 +1,358 @@ +package cn.hutool.core.codec; + +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.CharsetUtil; + +/** + * Base64工具类,提供Base64的编码和解码方案
+ * base64编码是用64(2的6次方)个ASCII字符来表示256(2的8次方)个ASCII字符,
+ * 也就是三位二进制数组经过编码后变为四位的ASCII字符显示,长度比原来增加1/3。 + * + * @author Looly + * + */ +public class Base64 { + + // -------------------------------------------------------------------- encode + /** + * 编码为Base64,非URL安全的 + * + * @param arr 被编码的数组 + * @param lineSep 在76个char之后是CRLF还是EOF + * @return 编码后的bytes + */ + public static byte[] encode(byte[] arr, boolean lineSep) { + return Base64Encoder.encode(arr, lineSep); + } + + /** + * 编码为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 Base64Encoder.encodeUrlSafe(arr, lineSep); + } + + /** + * base64编码 + * + * @param source 被编码的base64字符串 + * @return 被加密后的字符串 + */ + public static String encode(CharSequence source) { + return Base64Encoder.encode(source); + } + + /** + * base64编码,URL安全 + * + * @param source 被编码的base64字符串 + * @return 被加密后的字符串 + * @since 3.0.6 + */ + public static String encodeUrlSafe(CharSequence source) { + return Base64Encoder.encodeUrlSafe(source); + } + + /** + * base64编码 + * + * @param source 被编码的base64字符串 + * @param charset 字符集 + * @return 被加密后的字符串 + */ + public static String encode(CharSequence source, String charset) { + return Base64Encoder.encode(source, CharsetUtil.charset(charset)); + } + + /** + * base64编码,URL安全 + * + * @param source 被编码的base64字符串 + * @param charset 字符集 + * @return 被加密后的字符串 + * @since 3.0.6 + */ + public static String encodeUrlSafe(CharSequence source, String charset) { + return Base64Encoder.encodeUrlSafe(source, CharsetUtil.charset(charset)); + } + + /** + * base64编码 + * + * @param source 被编码的base64字符串 + * @param charset 字符集 + * @return 被加密后的字符串 + */ + public static String encode(CharSequence source, Charset charset) { + return Base64Encoder.encode(source, charset); + } + + /** + * base64编码,URL安全的 + * + * @param source 被编码的base64字符串 + * @param charset 字符集 + * @return 被加密后的字符串 + * @since 3.0.6 + */ + public static String encodeUrlSafe(CharSequence source, Charset charset) { + return Base64Encoder.encodeUrlSafe(source, charset); + } + + /** + * base64编码 + * + * @param source 被编码的base64字符串 + * @return 被加密后的字符串 + */ + public static String encode(byte[] source) { + return Base64Encoder.encode(source); + } + + /** + * base64编码,URL安全的 + * + * @param source 被编码的base64字符串 + * @return 被加密后的字符串 + * @since 3.0.6 + */ + public static String encodeUrlSafe(byte[] source) { + return Base64Encoder.encodeUrlSafe(source); + } + + /** + * base64编码 + * + * @param in 被编码base64的流(一般为图片流或者文件流) + * @return 被加密后的字符串 + * @since 4.0.9 + */ + public static String encode(InputStream in) { + return Base64Encoder.encode(IoUtil.readBytes(in)); + } + + /** + * base64编码,URL安全的 + * + * @param in 被编码base64的流(一般为图片流或者文件流) + * @return 被加密后的字符串 + * @since 4.0.9 + */ + public static String encodeUrlSafe(InputStream in) { + return Base64Encoder.encodeUrlSafe(IoUtil.readBytes(in)); + } + + /** + * base64编码 + * + * @param file 被编码base64的文件 + * @return 被加密后的字符串 + * @since 4.0.9 + */ + public static String encode(File file) { + return Base64Encoder.encode(FileUtil.readBytes(file)); + } + + /** + * base64编码,URL安全的 + * + * @param file 被编码base64的文件 + * @return 被加密后的字符串 + * @since 4.0.9 + */ + public static String encodeUrlSafe(File file) { + return Base64Encoder.encodeUrlSafe(FileUtil.readBytes(file)); + } + + /** + * base64编码 + * + * @param source 被编码的base64字符串 + * @param charset 字符集 + * @return 被加密后的字符串 + * @deprecated 编码参数无意义,作废 + */ + @Deprecated + public static String encode(byte[] source, String charset) { + return Base64Encoder.encode(source); + } + + /** + * base64编码,URL安全的 + * + * @param source 被编码的base64字符串 + * @param charset 字符集 + * @return 被加密后的字符串 + * @since 3.0.6 + * @deprecated 编码参数无意义,作废 + */ + @Deprecated + public static String encodeUrlSafe(byte[] source, String charset) { + return Base64Encoder.encodeUrlSafe(source); + } + + /** + * base64编码 + * + * @param source 被编码的base64字符串 + * @param charset 字符集 + * @return 被加密后的字符串 + * @deprecated 编码参数无意义,作废 + */ + @Deprecated + public static String encode(byte[] source, Charset charset) { + return Base64Encoder.encode(source); + } + + /** + * base64编码,URL安全的 + * + * @param source 被编码的base64字符串 + * @param charset 字符集 + * @return 被加密后的字符串 + * @since 3.0.6 + * @deprecated 编码参数无意义,作废 + */ + @Deprecated + public static String encodeUrlSafe(byte[] source, Charset charset) { + return Base64Encoder.encodeUrlSafe(source); + } + + /** + * 编码为Base64
+ * 如果isMultiLine为true,则每76个字符一个换行符,否则在一行显示 + * + * @param arr 被编码的数组 + * @param isMultiLine 在76个char之后是CRLF还是EOF + * @param isUrlSafe 是否使用URL安全字符,一般为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 Base64Decoder.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 被加密后的字符串 + */ + public static byte[] decode(CharSequence base64) { + return Base64Decoder.decode(base64); + } + + /** + * base64解码 + * + * @param source 被解码的base64字符串 + * @param charset 字符集 + * @return 被加密后的字符串 + *@deprecated 编码参数无意义,作废 + */ + @Deprecated + public static byte[] decode(CharSequence source, String charset) { + return Base64Decoder.decode(source); + } + + /** + * base64解码 + * + * @param source 被解码的base64字符串 + * @param charset 字符集 + * @return 被加密后的字符串 + *@deprecated 编码参数无意义,作废 + */ + @Deprecated + public static byte[] decode(CharSequence source, Charset charset) { + return Base64Decoder.decode(source); + } + + /** + * 解码Base64 + * + * @param in 输入 + * @return 解码后的bytes + */ + public static byte[] decode(byte[] in) { + return Base64Decoder.decode(in); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/codec/Base64Decoder.java b/hutool-core/src/main/java/cn/hutool/core/codec/Base64Decoder.java new file mode 100644 index 000000000..ab759f3dd --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/codec/Base64Decoder.java @@ -0,0 +1,172 @@ +package cn.hutool.core.codec; + +import java.nio.charset.Charset; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 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_TABLE2 = { + // -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + // -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + // -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, + // 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, + // -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + // 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + // -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + // 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1 }; + 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 + -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 IntWrapper offset = new IntWrapper(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.value <= 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); + } + } + + // ----------------------------------------------------------------------------------------------- Private start + /** + * 获取下一个有效的byte字符 + * + * @param in 输入 + * @param pos 当前位置,调用此方法后此位置保持在有效字符的下一个位置 + * @param maxPos 最大位置 + * @return 有效字符,如果达到末尾返回 + */ + private static byte getNextValidDecodeByte(byte[] in, IntWrapper pos, int maxPos) { + byte base64Byte; + byte decodeByte; + while (pos.value <= maxPos) { + base64Byte = in[pos.value++]; + if (base64Byte > -1) { + decodeByte = DECODE_TABLE[base64Byte]; + if (decodeByte > -1) { + return decodeByte; + } + } + } + // padding if reached max position + return PADDING; + } + + /** + * int包装,使之可变 + * + * @author looly + * + */ + private static class IntWrapper { + int value; + + IntWrapper(int value) { + this.value = value; + } + } + // ----------------------------------------------------------------------------------------------- Private end +} diff --git a/hutool-core/src/main/java/cn/hutool/core/codec/Base64Encoder.java b/hutool-core/src/main/java/cn/hutool/core/codec/Base64Encoder.java new file mode 100644 index 000000000..33c2b700b --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/codec/Base64Encoder.java @@ -0,0 +1,194 @@ +package cn.hutool.core.codec; + +import java.nio.charset.Charset; + +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; + +/** + * Base64编码 + * + * @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为true,则每76个字符一个换行符,否则在一行显示 + * + * @param arr 被编码的数组 + * @param isMultiLine 在76个char之后是CRLF还是EOF + * @param isUrlSafe 是否使用URL安全字符,一般为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/hutool-core/src/main/java/cn/hutool/core/codec/Caesar.java b/hutool-core/src/main/java/cn/hutool/core/codec/Caesar.java new file mode 100644 index 000000000..53e537af2 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/codec/Caesar.java @@ -0,0 +1,84 @@ +package cn.hutool.core.codec; + +/** + * 凯撒密码实现
+ * 算法来自:https://github.com/zhaorenjie110/SymmetricEncryptionAndDecryption + * + * @author looly + * + */ +public class Caesar { + + // 26个字母表 + public static String table = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz"; + + /** + * 传入明文,加密得到密文 + * + * @param message 加密的消息 + * @param offset 偏移量 + * @return 加密后的内容 + */ + public static String encode(String message, int offset) { + final int len = message.length(); + final char[] plain = message.toCharArray(); + char c; + for (int i = 0; i < len; i++) { + c = message.charAt(i); + if (false == Character.isLetter(c)) { + continue; + } + plain[i] = encodeChar(c, offset); + } + return new String(plain); + } + + /** + * 传入明文解密到密文 + * + * @param ciphertext 密文 + * @return 解密后的内容 + */ + public static String decode(String ciphertext, int offset) { + final int len = ciphertext.length(); + final char[] plain = ciphertext.toCharArray(); + char c; + for (int i = 0; i < len; i++) { + c = ciphertext.charAt(i); + if (false == 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/hutool-core/src/main/java/cn/hutool/core/codec/Morse.java b/hutool-core/src/main/java/cn/hutool/core/codec/Morse.java new file mode 100644 index 000000000..3be48540e --- /dev/null +++ b/hutool-core/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(Integer.valueOf(abc), dict); + dictionaries.put(dict, Integer.valueOf(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 (false == 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/hutool-core/src/main/java/cn/hutool/core/codec/Rot.java b/hutool-core/src/main/java/cn/hutool/core/codec/Rot.java new file mode 100644 index 000000000..cbb1af028 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/codec/Rot.java @@ -0,0 +1,173 @@ +package cn.hutool.core.codec; + +/** + * 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 isEnocdeNumber 是否编码数字 + * @return 编码后的字符串 + */ + public static String encode13(String message, boolean isEnocdeNumber) { + return encode(message, 13, isEnocdeNumber); + } + + /** + * RotN编码 + * + * @param message 被编码的消息 + * @param offset 位移,常用位移13 + * @param isEnocdeNumber 是否编码数字 + * @return 编码后的字符串 + */ + public static String encode(String message, int offset, boolean isEnocdeNumber) { + 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, isEnocdeNumber); + } + 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) { + 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/hutool-core/src/main/java/cn/hutool/core/codec/package-info.java b/hutool-core/src/main/java/cn/hutool/core/codec/package-info.java new file mode 100644 index 000000000..1d1a23584 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/collection/ArrayIter.java b/hutool-core/src/main/java/cn/hutool/core/collection/ArrayIter.java new file mode 100644 index 000000000..1f48f1eeb --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/collection/ArrayIter.java @@ -0,0 +1,115 @@ +package cn.hutool.core.collection; + +import java.io.Serializable; +import java.lang.reflect.Array; +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * 数组Iterator对象 + * @author Looly + * + * @param 元素类型 + * @since 4.1.1 + */ +public class ArrayIter implements Iterator, Iterable, Serializable{ + private static final long serialVersionUID = 1L; + + /** 数组 */ + private Object array; + /** 起始位置 */ + private int startIndex = 0; + /** 结束位置 */ + private int endIndex = 0; + /** 当前位置 */ + private int index = 0; + + /** + * 构造 + * @param array 数组 + * @throws IllegalArgumentException array对象不为数组抛出此异常 + * @throws NullPointerException array对象为null + */ + public ArrayIter(final Object array) { + this(array, 0); + } + + /** + * 构造 + * @param array 数组 + * @param startIndex 起始位置,当起始位置小于0或者大于结束位置,置为0。 + * @throws IllegalArgumentException array对象不为数组抛出此异常 + * @throws NullPointerException array对象为null + */ + public ArrayIter(final Object array, final 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() == false) { + 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; + } + + /** + * 重置数组位置 + */ + public void reset() { + this.index = this.startIndex; + } + + @Override + public Iterator iterator() { + return this; + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/collection/BoundedPriorityQueue.java b/hutool-core/src/main/java/cn/hutool/core/collection/BoundedPriorityQueue.java new file mode 100644 index 000000000..7630b32c5 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/collection/BoundedPriorityQueue.java @@ -0,0 +1,96 @@ +package cn.hutool.core.collection; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +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 int capacity; + private Comparator comparator; + + public BoundedPriorityQueue(int capacity) { + this(capacity, null); + } + + /** + * 构造 + * @param capacity 容量 + * @param comparator 比较器 + */ + public BoundedPriorityQueue(int capacity, final Comparator comparator) { + super(capacity, new Comparator(){ + + @Override + public int compare(E o1, E o2) { + int cResult = 0; + 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); + Collections.sort(list, comparator); + return list; + } + + @Override + public Iterator iterator() { + return toList().iterator(); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java b/hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java new file mode 100644 index 000000000..2bd8bba4c --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java @@ -0,0 +1,2514 @@ +package cn.hutool.core.collection; + +import java.lang.reflect.Type; +import java.util.AbstractCollection; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Deque; +import java.util.EnumSet; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.Stack; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.LinkedBlockingDeque; + +import cn.hutool.core.bean.BeanUtil; +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.Editor; +import cn.hutool.core.lang.Filter; +import cn.hutool.core.lang.Matcher; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.PageUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.TypeUtil; + +/** + * 集合相关工具类 + *

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

+ * 由于{@link Collection} 实现了{@link Iterable}接口,因此部分工具此类不提供,而是在{@link IterUtil} 中提供 + * + * @author xiaoleilu + * @since 3.1.1 + * @see IterUtil + */ +public class CollUtil { + + /** + * 两个集合的并集
+ * 针对一个集合中存在多个相同元素的情况,计算两个集合中此元素的个数,保留最多的个数
+ * 例如:集合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) { + final ArrayList list = new ArrayList<>(); + if (isEmpty(coll1)) { + list.addAll(coll2); + } else if (isEmpty(coll2)) { + list.addAll(coll1); + } else { + 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) { + union = union(union, coll); + } + return union; + } + + /** + * 两个集合的交集
+ * 针对一个集合中存在多个相同元素的情况,计算两个集合中此元素的个数,保留最少的个数
+ * 例如:集合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) { + final ArrayList list = new ArrayList<>(); + if (isNotEmpty(coll1) && isNotEmpty(coll2)) { + 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; + } + + /** + * 多个集合的交集
+ * 针对一个集合中存在多个相同元素的情况,计算两个集合中此元素的个数,保留最少的个数
+ * 例如:集合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]
+ * 结果:[c],此结果中只保留了一个
+ * 任意一个集合为空,返回另一个集合
+ * 两个集合无交集则返回两个集合的组合 + * + * @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 ArrayList 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; + } + + /** + * 判断指定集合是否包含指定值,如果集合为空(null或者空),返回{@code false},否则找到元素返回{@code true} + * + * @param collection 集合 + * @param value 需要查找的值 + * @return 如果集合为空(null或者空),返回{@code false},否则找到元素返回{@code true} + * @since 4.1.10 + */ + public static boolean contains(Collection collection, Object value) { + return isNotEmpty(collection) && collection.contains(value); + } + + /** + * 其中一个集合在另一个集合中是否至少包含一个元素,既是两个集合是否至少有一个共同的元素 + * + * @param coll1 集合1 + * @param coll2 集合2 + * @return 其中一个集合在另一个集合中是否至少包含一个元素 + * @since 2.1 + * @see #intersection + */ + 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) || isEmpty(coll2) || coll1.size() < coll2.size()) { + return false; + } + + for (Object object : coll2) { + if (false == 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(Iterable) + */ + public static Map countMap(Iterable collection) { + return IterUtil.countMap(collection); + } + + /** + * 以 conjunction 为分隔符将集合转换为字符串
+ * 如果集合元素为数组、{@link Iterable}或{@link Iterator},则递归组合其为字符串 + * + * @param 集合元素类型 + * @param iterable {@link Iterable} + * @param conjunction 分隔符 + * @return 连接后的字符串 + * @see IterUtil#join(Iterable, CharSequence) + */ + public static String join(Iterable iterable, CharSequence conjunction) { + return IterUtil.join(iterable, conjunction); + } + + /** + * 以 conjunction 为分隔符将集合转换为字符串
+ * 如果集合元素为数组、{@link Iterable}或{@link Iterator},则递归组合其为字符串 + * + * @param 集合元素类型 + * @param iterator 集合 + * @param conjunction 分隔符 + * @return 连接后的字符串 + * @see IterUtil#join(Iterator, CharSequence) + */ + 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 null; + } + + 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 null; + } + + 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 HashMap + /** + * 新建一个HashMap + * + * @param Key类型 + * @param Value类型 + * @return HashMap对象 + * @see MapUtil#newHashMap() + */ + public static HashMap newHashMap() { + return MapUtil.newHashMap(); + } + + /** + * 新建一个HashMap + * + * @param Key类型 + * @param Value类型 + * @param size 初始大小,由于默认负载因子0.75,传入的size会实际初始大小为size / 0.75 + * @param isOrder Map的Key是否有序,有序返回 {@link LinkedHashMap},否则返回 {@link HashMap} + * @return HashMap对象 + * @since 3.0.4 + * @see MapUtil#newHashMap(int, boolean) + */ + public static HashMap newHashMap(int size, boolean isOrder) { + return MapUtil.newHashMap(size, isOrder); + } + + /** + * 新建一个HashMap + * + * @param Key类型 + * @param Value类型 + * @param size 初始大小,由于默认负载因子0.75,传入的size会实际初始大小为size / 0.75 + * @return HashMap对象 + * @see MapUtil#newHashMap(int) + */ + public static HashMap newHashMap(int size) { + return MapUtil.newHashMap(size); + } + + // ----------------------------------------------------------------------------------------------- new HashSet + /** + * 新建一个HashSet + * + * @param 集合元素类型 + * @param ts 元素数组 + * @return HashSet对象 + */ + @SafeVarargs + public static HashSet newHashSet(T... ts) { + return newHashSet(false, ts); + } + + /** + * 新建一个LinkedHashSet + * + * @param 集合元素类型 + * @param ts 元素数组 + * @return HashSet对象 + * @since 4.1.10 + */ + @SafeVarargs + public static LinkedHashSet newLinkedHashSet(T... ts) { + return (LinkedHashSet) newHashSet(true, ts); + } + + /** + * 新建一个HashSet + * + * @param 集合元素类型 + * @param isSorted 是否有序,有序返回 {@link LinkedHashSet},否则返回 {@link HashSet} + * @param ts 元素数组 + * @return HashSet对象 + */ + @SafeVarargs + public static HashSet newHashSet(boolean isSorted, T... ts) { + if (null == ts) { + return isSorted ? new LinkedHashSet() : new HashSet(); + } + int initialCapacity = Math.max((int) (ts.length / .75f) + 1, 16); + HashSet set = isSorted ? new LinkedHashSet(initialCapacity) : new HashSet(initialCapacity); + for (T t : ts) { + set.add(t); + } + 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 newHashSet(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 enumration {@link Enumeration} + * @return HashSet对象 + * @since 3.0.8 + */ + public static HashSet newHashSet(boolean isSorted, Enumeration enumration) { + if (null == enumration) { + return newHashSet(isSorted, (T[]) null); + } + final HashSet set = isSorted ? new LinkedHashSet() : new HashSet(); + while (enumration.hasMoreElements()) { + set.add(enumration.nextElement()); + } + return set; + } + + // ----------------------------------------------------------------------------------------------- List + /** + * 新建一个空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); + } + List arrayList = isLinked ? new LinkedList() : new ArrayList(values.length); + for (T t : values) { + arrayList.add(t); + } + 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()); + } + + /** + * 新建一个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) { + 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 newArrayList(T... values) { + return (ArrayList) list(false, values); + } + + /** + * 数组转为ArrayList + * + * @param 集合元素类型 + * @param values 数组 + * @return ArrayList对象 + * @since 4.0.11 + */ + @SafeVarargs + public static ArrayList toList(T... values) { + return newArrayList(values); + } + + /** + * 新建一个ArrayList + * + * @param 集合元素类型 + * @param collection 集合 + * @return ArrayList对象 + */ + public static ArrayList newArrayList(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 newArrayList(Iterable iterable) { + return (ArrayList) list(false, iterable); + } + + /** + * 新建一个ArrayList
+ * 提供的参数为null时返回空{@link ArrayList} + * + * @param 集合元素类型 + * @param iter {@link Iterator} + * @return ArrayList对象 + * @since 3.0.8 + */ + public static ArrayList newArrayList(Iterator iter) { + return (ArrayList) list(false, iter); + } + + /** + * 新建一个ArrayList
+ * 提供的参数为null时返回空{@link ArrayList} + * + * @param 集合元素类型 + * @param enumration {@link Enumeration} + * @return ArrayList对象 + * @since 3.0.8 + */ + public static ArrayList newArrayList(Enumeration enumration) { + return (ArrayList) list(false, enumration); + } + + // ----------------------------------------------------------------------new LinkedList + /** + * 新建LinkedList + * + * @param values 数组 + * @param 类型 + * @return LinkedList + * @since 4.1.2 + */ + @SafeVarargs + public static LinkedList newLinkedList(T... values) { + return (LinkedList) list(true, values); + } + + /** + * 新建一个CopyOnWriteArrayList + * + * @param 集合元素类型 + * @param collection 集合 + * @return {@link CopyOnWriteArrayList} + */ + public static CopyOnWriteArrayList newCopyOnWriteArrayList(Collection collection) { + return (null == collection) ? (new CopyOnWriteArrayList()) : (new CopyOnWriteArrayList(collection)); + } + + /** + * 新建{@link BlockingQueue}
+ * 在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。 + * + * @param capacity 容量 + * @param isLinked 是否为链表形式 + * @return {@link BlockingQueue} + * @since 3.3.0 + */ + public static BlockingQueue newBlockingQueue(int capacity, boolean isLinked) { + BlockingQueue queue; + if (isLinked) { + queue = new LinkedBlockingDeque<>(capacity); + } else { + queue = new ArrayBlockingQueue<>(capacity); + } + return queue; + } + + /** + * 创建新的集合对象 + * + * @param 集合类型 + * @param collectionType 集合类型 + * @return 集合类型对应的实例 + * @since 3.0.8 + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static Collection create(Class collectionType) { + Collection list = null; + 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<>(); + } else if (collectionType.isAssignableFrom(EnumSet.class)) { + list = (Collection) EnumSet.noneOf((Class) ClassUtil.getTypeArgument(collectionType)); + } + + // 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 (Exception e) { + throw new UtilException(e); + } + } + return list; + } + + /** + * 创建Map
+ * 传入抽象Map{@link AbstractMap}和{@link Map}类将默认创建{@link HashMap} + * + * @param map键类型 + * @param map值类型 + * @param mapType map类型 + * @return {@link Map}实例 + * @see MapUtil#createMap(Class) + */ + public static Map createMap(Class mapType) { + return MapUtil.createMap(mapType); + } + + /** + * 去重集合 + * + * @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)); + } + } + + /** + * 截取集合的部分 + * + * @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); + } + + /** + * 截取集合的部分 + * + * @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) { + return list.subList(start, end); + } + + final List result = new ArrayList<>(); + for (int i = start; i < end; i += step) { + result.add(list.get(i)); + } + return result; + } + + /** + * 截取集合的部分 + * + * @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 list 被截取的数组 + * @param start 开始位置(包含) + * @param end 结束位置(不包含) + * @param step 步进 + * @return 截取后的数组,当开始位置超过最大时,返回空集合 + * @since 4.0.6 + */ + public static List sub(Collection list, int start, int end, int step) { + if (list == null || list.isEmpty()) { + return null; + } + + return sub(new ArrayList(list), start, end, step); + } + + /** + * 对集合按照指定长度分段,每一个段为单独的集合,返回这个集合的列表 + * + * @param 集合元素类型 + * @param collection 集合 + * @param size 每个段的长度 + * @return 分段列表 + */ + public static List> split(Collection collection, int size) { + final List> result = new ArrayList<>(); + + 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 编辑器接口 + * @return 过滤后的集合 + */ + public static Collection filter(Collection collection, Editor editor) { + if (null == collection || null == editor) { + return collection; + } + + Collection collection2 = ObjectUtil.clone(collection); + try { + collection2.clear(); + } catch (UnsupportedOperationException e) { + // 克隆后的对象不支持清空,说明为不可变集合对象,使用默认的ArrayList保存结果 + collection2 = new ArrayList<>(); + } + + T modified; + for (T t : collection) { + modified = editor.edit(t); + if (null != modified) { + collection2.add(modified); + } + } + return collection2; + } + + /** + * 过滤
+ * 过滤过程通过传入的Editor实现来返回需要的元素内容,这个Editor实现可以实现以下功能: + * + *
+	 * 1、过滤出需要的对象,如果返回null表示这个元素对象抛弃
+	 * 2、修改元素对象,返回集合中为修改后的对象
+	 * 
+ * + * @param 集合元素类型 + * @param list 集合 + * @param editor 编辑器接口 + * @return 过滤后的数组 + * @since 4.1.8 + */ + public static List filter(List list, Editor editor) { + if (null == list || null == editor) { + return list; + } + + final List list2 = (list instanceof LinkedList) ? new LinkedList() : new ArrayList(list.size()); + T modified; + for (T t : list) { + modified = editor.edit(t); + if (null != modified) { + list2.add(modified); + } + } + return list2; + } + + /** + * 过滤
+ * 过滤过程通过传入的Filter实现来过滤返回需要的元素内容,这个Filter实现可以实现以下功能: + * + *
+	 * 1、过滤出需要的对象,{@link Filter#accept(Object)}方法返回true的对象将被加入结果集合中
+	 * 
+ * + * @param 集合元素类型 + * @param collection 集合 + * @param filter 过滤器 + * @return 过滤后的数组 + * @since 3.1.0 + */ + public static Collection filter(Collection collection, Filter filter) { + if (null == collection || null == filter) { + return collection; + } + + Collection collection2 = ObjectUtil.clone(collection); + try { + collection2.clear(); + } catch (UnsupportedOperationException e) { + // 克隆后的对象不支持清空,说明为不可变集合对象,使用默认的ArrayList保存结果 + collection2 = new ArrayList<>(); + } + + for (T t : collection) { + if (filter.accept(t)) { + collection2.add(t); + } + } + return collection2; + } + + /** + * 过滤
+ * 过滤过程通过传入的Filter实现来过滤返回需要的元素内容,这个Filter实现可以实现以下功能: + * + *
+	 * 1、过滤出需要的对象,{@link Filter#accept(Object)}方法返回true的对象将被加入结果集合中
+	 * 
+ * + * @param 集合元素类型 + * @param list 集合 + * @param filter 过滤器 + * @return 过滤后的数组 + * @since 4.1.8 + */ + public static List filter(List list, Filter filter) { + if (null == list || null == filter) { + return list; + } + final List list2 = (list instanceof LinkedList) ? new LinkedList() : new ArrayList(list.size()); + for (T t : list) { + if (filter.accept(t)) { + list2.add(t); + } + } + return list2; + } + + /** + * 去除{@code null} 元素 + * + * @param collection 集合 + * @return 处理后的集合 + * @since 3.2.2 + */ + public static Collection removeNull(Collection collection) { + return filter(collection, new Editor() { + @Override + public T edit(T t) { + // 返回null便不加入集合 + return t; + } + }); + } + + /** + * 去掉集合中的多个元素 + * + * @param collection 集合 + * @param elesRemoved 被去掉的元素数组 + * @return 原集合 + * @since 4.1.0 + */ + @SuppressWarnings("unchecked") + public static Collection removeAny(Collection collection, T... elesRemoved) { + collection.removeAll(newHashSet(elesRemoved)); + return collection; + } + + /** + * 去除{@code null}或者"" 元素 + * + * @param collection 集合 + * @return 处理后的集合 + * @since 3.2.2 + */ + public static Collection removeEmpty(Collection collection) { + return filter(collection, new Filter() { + @Override + public boolean accept(T t) { + return false == StrUtil.isEmpty(t); + } + }); + } + + /** + * 去除{@code null}或者""或者空白字符串 元素 + * + * @param collection 集合 + * @return 处理后的集合 + * @since 3.2.2 + */ + public static Collection removeBlank(Collection collection) { + return filter(collection, new Filter() { + @Override + public boolean accept(T t) { + return false == StrUtil.isBlank(t); + } + }); + } + + /** + * 通过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 抽取后的新列表 + * @since 4.5.7 + */ + public static List extract(Iterable collection, Editor editor, boolean ignoreNull) { + final List fieldValueList = new ArrayList<>(); + if(null == collection) { + return fieldValueList; + } + + Object value; + for (Object bean : collection) { + value = editor.edit(bean); + 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 extract(collection, new Editor() { + @Override + public Object edit(Object 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); + } + + /** + * 查找第一个匹配元素对象 + * + * @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, new Filter() { + @Override + public boolean accept(T 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); + } + }); + } + + /** + * 过滤
+ * 过滤过程通过传入的Editor实现来返回需要的元素内容,这个Editor实现可以实现以下功能: + * + *
+	 * 1、过滤出需要的对象,如果返回null表示这个元素对象抛弃
+	 * 2、修改元素对象,返回集合中为修改后的对象
+	 * 
+ * + * @param Key类型 + * @param Value类型 + * @param map Map + * @param editor 编辑器接口 + * @return 过滤后的Map + * @see MapUtil#filter(Map, Editor) + */ + public static Map filter(Map map, Editor> editor) { + return MapUtil.filter(map, editor); + } + + /** + * 过滤
+ * 过滤过程通过传入的Editor实现来返回需要的元素内容,这个Editor实现可以实现以下功能: + * + *
+	 * 1、过滤出需要的对象,如果返回null表示这个元素对象抛弃
+	 * 2、修改元素对象,返回集合中为修改后的对象
+	 * 
+ * + * @param Key类型 + * @param Value类型 + * @param map Map + * @param filter 编辑器接口 + * @return 过滤后的Map + * @since 3.1.0 + * @see MapUtil#filter(Map, Filter) + */ + public static Map filter(Map map, Filter> filter) { + return MapUtil.filter(map, filter); + } + + /** + * 集合中匹配规则的数量 + * + * @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; + } + + // ---------------------------------------------------------------------- isEmpty + /** + * 集合是否为空 + * + * @param collection 集合 + * @return 是否为空 + */ + public static boolean isEmpty(Collection collection) { + return collection == null || collection.isEmpty(); + } + + /** + * Map是否为空 + * + * @param map 集合 + * @return 是否为空 + * @see MapUtil#isEmpty(Map) + */ + public static boolean isEmpty(Map map) { + return MapUtil.isEmpty(map); + } + + /** + * 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 || false == enumeration.hasMoreElements(); + } + + // ---------------------------------------------------------------------- isNotEmpty + + /** + * 集合是否为非空 + * + * @param collection 集合 + * @return 是否为非空 + */ + public static boolean isNotEmpty(Collection collection) { + return false == isEmpty(collection); + } + + /** + * Map是否为非空 + * + * @param map 集合 + * @return 是否为非空 + * @see MapUtil#isNotEmpty(Map) + */ + public static boolean isNotEmpty(Map map) { + return MapUtil.isNotEmpty(map); + } + + /** + * 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} 返回false + * @return 是否包含{@code null}元素 + * @since 3.0.7 + * @see IterUtil#hasNull(Iterable) + */ + public static boolean hasNull(Iterable iterable) { + return IterUtil.hasNull(iterable); + } + + // ---------------------------------------------------------------------- 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.split(keys, delimiter), StrUtil.split(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 null; + } + + final List keyList = new ArrayList(keys); + final List valueList = new ArrayList(values); + + final int size = Math.min(keys.size(), values.size()); + final Map map = new HashMap((int) (size / 0.75)); + for (int i = 0; i < size; i++) { + map.put(keyList.get(i), valueList.get(i)); + } + + 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} + * @since 3.0.8 + * @see MapUtil#of(Object[]) + */ + 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); + for (T t : collection) { + treeSet.add(t); + } + return treeSet; + } + + /** + * Iterator转换为Enumeration + *

+ * Adapt the specified Iterator to the 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 Enumeration to the 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); + } + + /** + * 将指定对象全部加入到集合中
+ * 提供的对象如果为集合类型,会自动转换为目标元素类型
+ * + * @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())); + } + + /** + * 将指定对象全部加入到集合中
+ * 提供的对象如果为集合类型,会自动转换为目标元素类型
+ * + * @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.isUnknow(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按照逗号分隔的列表对待 + iter = StrUtil.splitTrim((CharSequence) value, CharUtil.COMMA).iterator(); + } else { + // 其它类型按照单一元素处理 + iter = CollUtil.newArrayList(value).iterator(); + } + + final ConverterRegistry convert = ConverterRegistry.getInstance(); + while (iter.hasNext()) { + collection.add((T) 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) { + 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) { + for (T value : values) { + collection.add(value); + } + } + return collection; + } + + /** + * 将另一个列表中的元素加入到列表中,如果列表中已经存在此元素则忽略之 + * + * @param 集合元素类型 + * @param list 列表 + * @param otherList 其它列表 + * @return 此列表 + */ + public static List addAllIfNotContains(List list, List otherList) { + for (T t : otherList) { + if (false == 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) { + return null; + } + + if (collection instanceof List) { + final List list = ((List) collection); + return list.get(index); + } else { + int i = 0; + for (T t : collection) { + if (i > index) { + break; + } else if (i == index) { + return t; + } + i++; + } + } + return null; + } + + /** + * 获取集合中指定多个下标的元素值,下标可以为负数,例如-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 { + Object[] array = ((Collection) collection).toArray(); + for (int index : indexes) { + if (index < 0) { + index += size; + } + result.add((T) array[index]); + } + } + return result; + } + + /** + * 获取集合的第一个元素 + * + * @param 集合元素类型 + * @param iterable {@link Iterable} + * @return 第一个元素 + * @since 3.0.1 + * @see IterUtil#getFirst(Iterable) + */ + public static T getFirst(Iterable iterable) { + return IterUtil.getFirst(iterable); + } + + /** + * 获取集合的第一个元素 + * + * @param 集合元素类型 + * @param iterator {@link Iterator} + * @return 第一个元素 + * @since 3.0.1 + * @see IterUtil#getFirst(Iterator) + */ + 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 + * @since 3.0.8 + * @see IterUtil#getElementType(Iterable) + */ + public static Class getElementType(Iterable iterable) { + return IterUtil.getElementType(iterable); + } + + /** + * 获得{@link Iterator}对象的元素类型(通过第一个非空元素判断) + * + * @param iterator {@link Iterator} + * @return 元素类型,当列表为空或元素全部为null时,返回null + * @since 3.0.8 + * @see IterUtil#getElementType(Iterator) + */ + 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) { + final ArrayList values = new ArrayList<>(); + for (K k : keys) { + values.add(map.get(k)); + } + return values; + } + + /** + * 从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) { + final ArrayList values = new ArrayList<>(); + while (keys.hasNext()) { + values.add(map.get(keys.next())); + } + return values; + } + + // ------------------------------------------------------------------------------------------------- sort + /** + * 将多个集合排序并显示不同的段落(分页)
+ * 采用{@link BoundedPriorityQueue}实现分页取局部 + * + * @param 集合元素类型 + * @param pageNo 页码,从1开始计数,0和1效果相同 + * @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) { + Collections.sort(list, comparator); + } + + return page(pageNo, pageSize, list); + } + + /** + * 对指定List分页取值 + * + * @param 集合元素类型 + * @param pageNo 页码,从1开始计数,0和1效果相同 + * @param pageSize 每页的条目数 + * @param list 列表 + * @return 分页后的段落内容 + * @since 4.1.20 + */ + public static List page(int pageNo, int pageSize, List list) { + if (isEmpty(list)) { + return new ArrayList<>(0); + } + + int resultSize = list.size(); + // 每页条目数大于总数直接返回所有 + if (resultSize <= pageSize) { + if (pageNo <= 1) { + return Collections.unmodifiableList(list); + } else { + // 越界直接返回空 + return new ArrayList<>(0); + } + } + final int[] startEnd = PageUtil.transToStartEnd(pageNo, pageSize); + if (startEnd[1] > resultSize) { + startEnd[1] = resultSize; + } + return list.subList(startEnd[0], startEnd[1]); + } + + /** + * 排序集合,排序不会修改原集合 + * + * @param 集合元素类型 + * @param collection 集合 + * @param comparator 比较器 + * @return treeSet + */ + public static List sort(Collection collection, Comparator comparator) { + List list = new ArrayList(collection); + Collections.sort(list, 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) { + Collections.sort(list, c); + return list; + } + + /** + * 根据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 sort(list, new PropertyComparator<>(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 sort(list, new PinyinComparator()); + } + + /** + * 排序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); + Collections.sort(list, comparator); + + LinkedHashMap result = new LinkedHashMap<>(); + for (Map.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 + */ + public static List> sortEntryToList(Collection> collection) { + List> list = new LinkedList<>(collection); + Collections.sort(list, new Comparator>() { + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public int compare(Entry o1, Entry 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 Iterator},使用{@link Consumer} 接受遍历的每条数据,并针对每条数据做处理 + * + * @param 集合元素类型 + * @param iterator {@link Iterator} + * @param consumer {@link Consumer} 遍历的每条数据处理器 + */ + public static void forEach(Iterator iterator, Consumer consumer) { + 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) { + int index = 0; + while (enumeration.hasMoreElements()) { + consumer.accept(enumeration.nextElement(), index); + index++; + } + } + + /** + * 循环遍历Map,使用{@link KVConsumer} 接受遍历的每条数据,并针对每条数据做处理 + * + * @param Key类型 + * @param Value类型 + * @param map {@link Map} + * @param kvConsumer {@link KVConsumer} 遍历的每条数据处理器 + */ + public static void forEach(Map map, KVConsumer kvConsumer) { + int index = 0; + for (Entry entry : map.entrySet()) { + kvConsumer.accept(entry.getKey(), entry.getValue(), index); + index++; + } + } + + /** + * 分组,按照{@link Hash}接口定义的hash算法,集合中的元素放入hash值对应的子列表中 + * + * @param 元素类型 + * @param collection 被分组的集合 + * @param hash Hash值算法,决定元素放在第几个分组的规则 + * @return 分组后的集合 + */ + public static List> group(Collection collection, Hash hash) { + final List> result = new ArrayList<>(); + if (isEmpty(collection)) { + return result; + } + if (null == hash) { + // 默认hash算法,按照元素的hashCode分组 + hash = new Hash() { + @Override + public int hash(T t) { + return null == t ? 0 : t.hashCode(); + } + }; + } + + int index; + List subList; + for (T t : collection) { + index = hash.hash(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 Hash() { + private List fieldNameList = new ArrayList<>(); + + @Override + public int hash(T t) { + if (null == t || false == 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) { + Collections.reverse(list); + return list; + } + + /** + * 反序给定List,会创建一个新的List,原List数据不变 + * + * @param 元素类型 + * @param list 被反转的List + * @return 反转后的List + * @since 4.0.6 + */ + public static List reverseNew(List list) { + final List list2 = ObjectUtil.clone(list); + return reverse(list2); + } + + /** + * 设置或增加元素。当index小于List的长度时,替换指定位置的值,否则在尾部追加 + * + * @param list List列表 + * @param index 位置 + * @param element 新元素 + * @return 原List + * @since 4.1.2 + */ + public static List setOrAppend(List list, int index, T element) { + if (index < list.size()) { + list.set(index, element); + } else { + list.add(element); + } + return list; + } + + /** + * 获取指定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; + } + + // ---------------------------------------------------------------------------------------------- Interface start + /** + * 针对一个参数做相应的操作 + * + * @author Looly + * + * @param 处理参数类型 + */ + public static interface Consumer { + /** + * 接受并处理一个参数 + * + * @param value 参数值 + * @param index 参数在集合中的索引 + */ + void accept(T value, int index); + } + + /** + * 针对两个参数做相应的操作,例如Map中的KEY和VALUE + * + * @author Looly + * + * @param KEY类型 + * @param VALUE类型 + */ + public static interface KVConsumer { + /** + * 接受并处理一对参数 + * + * @param key 键 + * @param value 值 + * @param index 参数在集合中的索引 + */ + void accept(K key, V value, int index); + } + + /** + * Hash计算接口 + * + * @author looly + * + * @param 被计算hash的对象类型 + * @since 3.2.2 + */ + public static interface Hash { + /** + * 计算Hash值 + * + * @param t 对象 + * @return hash + */ + int hash(T t); + } + // ---------------------------------------------------------------------------------------------- Interface end +} diff --git a/hutool-core/src/main/java/cn/hutool/core/collection/CollectionUtil.java b/hutool-core/src/main/java/cn/hutool/core/collection/CollectionUtil.java new file mode 100644 index 000000000..14724f29a --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/collection/ConcurrentHashSet.java b/hutool-core/src/main/java/cn/hutool/core/collection/ConcurrentHashSet.java new file mode 100644 index 000000000..8787f47b9 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/collection/ConcurrentHashSet.java @@ -0,0 +1,115 @@ +package cn.hutool.core.collection; + +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Iterator; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 通过{@link ConcurrentHashMap}实现的线程安全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 Object PRESENT = new Object(); + private final ConcurrentHashMap map; + + // ----------------------------------------------------------------------------------- Constructor start + /** + * 构造
+ * 触发因子为默认的0.75 + */ + public ConcurrentHashSet() { + map = new ConcurrentHashMap<>(); + } + + /** + * 构造
+ * 触发因子为默认的0.75 + * + * @param initialCapacity 初始大小 + */ + public ConcurrentHashSet(int initialCapacity) { + map = new ConcurrentHashMap<>(initialCapacity); + } + + /** + * 构造 + * + * @param initialCapacity 初始大小 + * @param loadFactor 加载因子。此参数决定数据增长时触发的百分比 + */ + public ConcurrentHashSet(int initialCapacity, float loadFactor) { + map = new ConcurrentHashMap<>(initialCapacity, loadFactor); + } + + /** + * 构造 + * + * @param initialCapacity 初始大小 + * @param loadFactor 触发因子。此参数决定数据增长时触发的百分比 + * @param concurrencyLevel 线程并发度 + */ + public ConcurrentHashSet(int initialCapacity, float loadFactor, int concurrencyLevel) { + map = new ConcurrentHashMap<>(initialCapacity, loadFactor, concurrencyLevel); + } + + /** + * 从已有集合中构造 + * @param iter {@link Iterable} + */ + public ConcurrentHashSet(Iterable iter) { + if(iter instanceof Collection) { + final Collection collection = (Collection)iter; + map = new ConcurrentHashMap<>((int)(collection.size() / 0.75f)); + this.addAll(collection); + }else { + map = new ConcurrentHashMap<>(); + 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) { + return map.containsKey(o); + } + + @Override + public boolean add(E e) { + return map.put(e, PRESENT) == null; + } + + @Override + public boolean remove(Object o) { + return map.remove(o) == PRESENT; + } + + @Override + public void clear() { + map.clear(); + } +} \ No newline at end of file diff --git a/hutool-core/src/main/java/cn/hutool/core/collection/CopiedIter.java b/hutool-core/src/main/java/cn/hutool/core/collection/CopiedIter.java new file mode 100644 index 000000000..9fb2fe79f --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/collection/CopiedIter.java @@ -0,0 +1,70 @@ +package cn.hutool.core.collection; + +import java.io.Serializable; +import java.util.Iterator; +import java.util.LinkedList; +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 Iterator, Iterable, Serializable { + private static final long serialVersionUID = 1L; + + private List eleList = new LinkedList<>(); + private Iterator listIterator; + + public static CopiedIter copyOf(Iterator iterator){ + return new CopiedIter<>(iterator); + } + + /** + * 构造 + * @param iterator 被复制的Iterator + */ + public CopiedIter(Iterator iterator) { + while (iterator.hasNext()) { + eleList.add(iterator.next()); + } + 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."); + } + + @Override + public Iterator iterator() { + return this; + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/collection/EnumerationIter.java b/hutool-core/src/main/java/cn/hutool/core/collection/EnumerationIter.java new file mode 100644 index 000000000..bf70ddc8a --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/collection/EnumerationIter.java @@ -0,0 +1,47 @@ +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 Iterator, Iterable, 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(); + } + + @Override + public Iterator iterator() { + return this; + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/collection/IterUtil.java b/hutool-core/src/main/java/cn/hutool/core/collection/IterUtil.java new file mode 100644 index 000000000..5689a7929 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/collection/IterUtil.java @@ -0,0 +1,581 @@ +package cn.hutool.core.collection; + +import java.util.ArrayList; +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 cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; + +/** + * {@link Iterable} 和 {@link Iterator} 相关工具类 + * + * @author Looly + * @since 3.1.0 + */ +public class IterUtil { + + /** + * 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 || false == 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} 返回false + * @return 是否包含{@code null}元素 + */ + public static boolean hasNull(Iterable iter) { + return hasNull(null == iter ? null : iter.iterator()); + } + + /** + * 是否包含{@code null}元素 + * + * @param iter 被检查的{@link Iterator}对象,如果为{@code null} 返回false + * @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) { + if (null == iter) { + return true; + } + + while (iter.hasNext()) { + if (null != iter.next()) { + return false; + } + } + return true; + } + + /** + * 根据集合返回一个元素计数的 {@link Map}
+ * 所谓元素计数就是假如这个集合中某个元素出现了n次,那将这个元素做为key,n做为value
+ * 例如:[a,b,c,c,c] 得到:
+ * a: 1
+ * b: 1
+ * c: 3
+ * + * @param 集合元素类型 + * @param iter {@link Iterable},如果为null返回一个空的Map + * @return {@link Map} + */ + public static Map countMap(Iterable iter) { + return countMap(null == iter ? null : iter.iterator()); + } + + /** + * 根据集合返回一个元素计数的 {@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) { + Integer count; + T t; + while (iter.hasNext()) { + t = iter.next(); + count = countMap.get(t); + if (null == count) { + countMap.put(t, 1); + } else { + countMap.put(t, count + 1); + } + } + } + return countMap; + } + + /** + * 字段值与列表值对应的Map,常用于元素对象中有唯一ID时需要按照这个ID查找对象的情况
+ * 例如:车牌号 =》车 + * + * @param 字段名对应值得类型,不确定请使用Object + * @param 对象类型 + * @param iter 对象列表 + * @param fieldName 字段名(会通过反射获取其值) + * @return 某个字段值与对象对应Map + * @since 4.0.4 + */ + public static Map fieldValueMap(Iterable iter, String fieldName) { + return fieldValueMap(null == iter ? null : iter.iterator(), fieldName); + } + + /** + * 字段值与列表值对应的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) { + final Map result = new HashMap<>(); + if (null != iter) { + V value; + while (iter.hasNext()) { + value = iter.next(); + result.put((K) ReflectUtil.getFieldValue(value, fieldName), value); + } + } + return result; + } + + /** + * 两个字段值组成新的Map + * + * @param 字段名对应值得类型,不确定请使用Object + * @param 值类型,不确定使用Object + * @param iterable 对象列表 + * @param fieldNameForKey 做为键的字段名(会通过反射获取其值) + * @param fieldNameForValue 做为值的字段名(会通过反射获取其值) + * @return 某个字段值与对象对应Map + * @since 4.6.2 + */ + public static Map fieldValueAsMap(Iterable iterable, String fieldNameForKey, String fieldNameForValue) { + return fieldValueAsMap(null == iterable ? null : iterable.iterator(), fieldNameForKey, fieldNameForValue); + } + + /** + * 两个字段值组成新的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) { + final Map result = new HashMap<>(); + if (null != iter) { + Object value; + while (iter.hasNext()) { + value = iter.next(); + result.put((K) ReflectUtil.getFieldValue(value, fieldNameForKey), (V) ReflectUtil.getFieldValue(value, fieldNameForValue)); + } + } + return result; + } + + /** + * 获取指定Bean列表中某个字段,生成新的列表 + * + * @param 对象类型 + * @param iterable 对象列表 + * @param fieldName 字段名(会通过反射获取其值) + * @return 某个字段值与对象对应Map + * @since 4.6.2 + */ + public static List fieldValueList(Iterable iterable, String fieldName) { + return fieldValueList(null == iterable ? null : iterable.iterator(), 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 为分隔符将集合转换为字符串 + * + * @param 集合元素类型 + * @param iterable {@link Iterable} + * @param conjunction 分隔符 + * @return 连接后的字符串 + */ + public static String join(Iterable iterable, CharSequence conjunction) { + if (null == iterable) { + return null; + } + return join(iterable.iterator(), conjunction); + } + + /** + * 以 conjunction 为分隔符将集合转换为字符串 + * + * @param 集合元素类型 + * @param iterable {@link Iterable} + * @param conjunction 分隔符 + * @param prefix 每个元素添加的前缀,null表示不添加 + * @param suffix 每个元素添加的后缀,null表示不添加 + * @return 连接后的字符串 + * @since 4.0.10 + */ + public static String join(Iterable iterable, CharSequence conjunction, String prefix, String suffix) { + if (null == iterable) { + return null; + } + return join(iterable.iterator(), conjunction, prefix, suffix); + } + + /** + * 以 conjunction 为分隔符将集合转换为字符串
+ * 如果集合元素为数组、{@link Iterable}或{@link Iterator},则递归组合其为字符串 + * + * @param 集合元素类型 + * @param iterator 集合 + * @param conjunction 分隔符 + * @return 连接后的字符串 + */ + public static String join(Iterator iterator, CharSequence conjunction) { + return join(iterator, conjunction, null, null); + } + + /** + * 以 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) { + if (null == iterator) { + return null; + } + + final StringBuilder sb = new StringBuilder(); + boolean isFirst = true; + T item; + while (iterator.hasNext()) { + if (isFirst) { + isFirst = false; + } else { + sb.append(conjunction); + } + + item = iterator.next(); + if (ArrayUtil.isArray(item)) { + sb.append(ArrayUtil.join(ArrayUtil.wrap(item), conjunction, prefix, suffix)); + } else if (item instanceof Iterable) { + sb.append(join((Iterable) item, conjunction, prefix, suffix)); + } else if (item instanceof Iterator) { + sb.append(join((Iterator) item, conjunction, prefix, suffix)); + } else { + sb.append(StrUtil.wrap(String.valueOf(item), prefix, suffix)); + } + } + return sb.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; + } + + /** + * Iterator转List
+ * 不判断,直接生成新的List + * + * @param 元素类型 + * @param iter {@link Iterator} + * @return List + * @since 4.0.6 + */ + public static List toList(Iterable iter) { + return toList(iter.iterator()); + } + + /** + * Iterator转List
+ * 不判断,直接生成新的List + * + * @param 元素类型 + * @param iter {@link Iterator} + * @return List + * @since 4.0.6 + */ + public static List toList(Iterator iter) { + final List list = new ArrayList<>(); + while (iter.hasNext()) { + list.add(iter.next()); + } + return list; + } + + /** + * Enumeration转换为Iterator + *

+ * Adapt the specified Enumeration to the 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 new Iterable() { + @Override + public Iterator iterator() { + return iter; + } + }; + } + + /** + * 获取集合的第一个元素 + * + * @param 集合元素类型 + * @param iterable {@link Iterable} + * @return 第一个元素 + */ + public static T getFirst(Iterable iterable) { + if (null != iterable) { + return getFirst(iterable.iterator()); + } + return null; + } + + /** + * 获取集合的第一个元素 + * + * @param 集合元素类型 + * @param iterator {@link Iterator} + * @return 第一个元素 + */ + public static T getFirst(Iterator iterator) { + if (null != iterator && iterator.hasNext()) { + return iterator.next(); + } + return null; + } + + /** + * 获得{@link Iterable}对象的元素类型(通过第一个非空元素判断)
+ * 注意,此方法至少会调用多次next方法 + * + * @param iterable {@link Iterable} + * @return 元素类型,当列表为空或元素全部为null时,返回null + */ + public static Class getElementType(Iterable iterable) { + if (null != iterable) { + final Iterator iterator = iterable.iterator(); + return getElementType(iterator); + } + return null; + } + + /** + * 获得{@link Iterator}对象的元素类型(通过第一个非空元素判断)
+ * 注意,此方法至少会调用多次next方法 + * + * @param iterator {@link Iterator} + * @return 元素类型,当列表为空或元素全部为null时,返回null + */ + public static Class getElementType(Iterator iterator) { + final Iterator iter2 = new CopiedIter<>(iterator); + if (null != iter2) { + Object t; + while (iter2.hasNext()) { + t = iter2.next(); + if (null != t) { + return t.getClass(); + } + } + } + return null; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/collection/IteratorEnumeration.java b/hutool-core/src/main/java/cn/hutool/core/collection/IteratorEnumeration.java new file mode 100644 index 000000000..2a61546fc --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/collection/LineIter.java b/hutool-core/src/main/java/cn/hutool/core/collection/LineIter.java new file mode 100644 index 000000000..06965cf2a --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/collection/LineIter.java @@ -0,0 +1,163 @@ +package cn.hutool.core.collection; + +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; +import java.util.Iterator; +import java.util.NoSuchElementException; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.lang.Assert; + +/** + * 将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 implements Iterator, Iterable, Closeable, Serializable { + private static final long serialVersionUID = 1L; + + /** The reader that is being read. */ + private final BufferedReader bufferedReader; + /** The current line. */ + private String cachedLine; + /** A flag indicating if the iterator has been fully read. */ + private boolean finished = false; + + /** + * 构造 + * + * @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); + } + + // ----------------------------------------------------------------------- + /** + * 判断{@link Reader}是否可以存在下一行。 If there is an IOException then {@link #close()} will be called on this instance. + * + * @return {@code true} 表示有更多行 + * @throws IORuntimeException IO异常 + */ + @Override + public boolean hasNext() throws IORuntimeException { + if (cachedLine != null) { + return true; + } else if (finished) { + return false; + } else { + try { + while (true) { + String line = bufferedReader.readLine(); + if (line == null) { + finished = true; + return false; + } else if (isValidLine(line)) { + cachedLine = line; + return true; + } + } + } catch (IOException ioe) { + close(); + throw new IORuntimeException(ioe); + } + } + } + + /** + * 返回下一行内容 + * + * @return 下一行内容 + * @throws NoSuchElementException 没有新行 + */ + @Override + public String next() throws NoSuchElementException { + return nextLine(); + } + + /** + * 返回下一行 + * + * @return 下一行 + * @throws NoSuchElementException 没有更多行 + */ + public String nextLine() throws NoSuchElementException { + if (false == hasNext()) { + throw new NoSuchElementException("No more lines"); + } + String currentLine = this.cachedLine; + this.cachedLine = null; + return currentLine; + } + + /** + * 关闭Reader + */ + @Override + public void close() { + finished = true; + IoUtil.close(bufferedReader); + cachedLine = null; + } + + /** + * 不支持移除 + * + * @throws UnsupportedOperationException 始终抛出此异常 + */ + @Override + public void remove() { + throw new UnsupportedOperationException("Remove unsupported on LineIterator"); + } + + /** + * 重写此方法来判断是否每一行都被返回,默认全部为true + * + * @param line 需要验证的行 + * @return 是否通过验证 + */ + protected boolean isValidLine(String line) { + return true; + } + + @Override + public Iterator iterator() { + return this; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/collection/package-info.java b/hutool-core/src/main/java/cn/hutool/core/collection/package-info.java new file mode 100644 index 000000000..593a7e352 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/comparator/ComparableComparator.java b/hutool-core/src/main/java/cn/hutool/core/comparator/ComparableComparator.java new file mode 100644 index 000000000..d76501fce --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/comparator/ComparableComparator.java @@ -0,0 +1,54 @@ +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() { + super(); + } + + /** + * 比较两个{@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/hutool-core/src/main/java/cn/hutool/core/comparator/ComparatorChain.java b/hutool-core/src/main/java/cn/hutool/core/comparator/ComparatorChain.java new file mode 100644 index 000000000..9dfaed0bd --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/comparator/ComparatorChain.java @@ -0,0 +1,280 @@ +package cn.hutool.core.comparator; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; + +import cn.hutool.core.lang.Chain; + +/** + * 比较器链。此链包装了多个比较器,最终比较结果按照比较器顺序综合多个比较器结果。
+ * 按照比较器链的顺序分别比较,如果比较出相等则转向下一个比较器,否则直接返回
+ * 此类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 BitSet orderingBits = null; + /** 比较器是否被锁定。锁定的比较器链不能再添加新的比较器。比较器会在开始比较时开始加锁。 */ + private boolean lock = false; + + /** + * 构造空的比较器链,必须至少有一个比较器,否则会在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 == true) { + 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 == true) { + orderingBits.set(chain.size() - 1); + } + return this; + } + + /** + * 替换指定位置的比较器,保持原排序方式 + * + * @param index 位置 + * @param comparator {@link Comparator} + * @return this + * @exception 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 == true) { + 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 == false) { + 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 (true == 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 (null == orderingBits ? null == otherChain.orderingBits : this.orderingBits.equals(otherChain.orderingBits)) // + && (null == otherChain ? null == otherChain.chain : this.chain.equals(otherChain.chain)); + } + return false; + } + + //------------------------------------------------------------------------------------------------------------------------------- Private method start + /** + * 被锁定时抛出异常 + * + * @throws UnsupportedOperationException 被锁定抛出此异常 + */ + private void checkLocked() { + if (lock == true) { + 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/hutool-core/src/main/java/cn/hutool/core/comparator/ComparatorException.java b/hutool-core/src/main/java/cn/hutool/core/comparator/ComparatorException.java new file mode 100644 index 000000000..67f3e9cef --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/comparator/CompareUtil.java b/hutool-core/src/main/java/cn/hutool/core/comparator/CompareUtil.java new file mode 100644 index 000000000..ee98bf667 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/comparator/CompareUtil.java @@ -0,0 +1,80 @@ +package cn.hutool.core.comparator; + +public class CompareUtil { + + /** + * {@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 java.util.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 java.util.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 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; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/comparator/FieldComparator.java b/hutool-core/src/main/java/cn/hutool/core/comparator/FieldComparator.java new file mode 100644 index 000000000..f22ab1448 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/comparator/FieldComparator.java @@ -0,0 +1,69 @@ +package cn.hutool.core.comparator; + +import java.io.Serializable; +import java.lang.reflect.Field; +import java.util.Comparator; + +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; + +/** + * Bean字段排序器
+ * 参阅feilong-core中的PropertyComparator + * + * @author Looly + * + * @param 被比较的Bean + */ +public class FieldComparator implements Comparator, Serializable { + private static final long serialVersionUID = 9157326766723846313L; + + private final Field field; + + /** + * 构造 + * + * @param beanClass Bean类 + * @param fieldName 字段名 + */ + public FieldComparator(Class beanClass, String fieldName) { + this.field = ClassUtil.getDeclaredField(beanClass, fieldName); + if(this.field == null){ + throw new IllegalArgumentException(StrUtil.format("Field [{}] not found in Class [{}]", fieldName, beanClass.getName())); + } + } + + @Override + public int compare(T o1, T o2) { + 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, this.field); + v2 = (Comparable) ReflectUtil.getFieldValue(o2, this.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/hutool-core/src/main/java/cn/hutool/core/comparator/IndexedComparator.java b/hutool-core/src/main/java/cn/hutool/core/comparator/IndexedComparator.java new file mode 100644 index 000000000..0b62eb2d5 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/comparator/IndexedComparator.java @@ -0,0 +1,41 @@ +package cn.hutool.core.comparator; + +import java.util.Comparator; + +import cn.hutool.core.util.ArrayUtil; + +/** + * 按照数组的顺序正序排列,数组的元素位置决定了对象的排序先后
+ * 如果参与排序的元素并不在数组中,则排序在前 + * + * @author looly + * + * @param 被排序元素类型 + * @since 4.1.5 + */ +public class IndexedComparator implements Comparator { + + private T[] array; + + /** + * 构造 + * + * @param objs 参与排序的数组,数组的元素位置决定了对象的排序先后 + */ + @SuppressWarnings("unchecked") + public IndexedComparator(T... objs) { + this.array = objs; + } + + @Override + public int compare(T o1, T o2) { + final int index1 = ArrayUtil.indexOf(array, o1); + final int index2 = ArrayUtil.indexOf(array, o2); + if(index1 == index2) { + //位置相同使用自然排序 + return CompareUtil.compare(o1, o2, true); + } + return index1 < index2 ? -1 : 1; + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/comparator/PinyinComparator.java b/hutool-core/src/main/java/cn/hutool/core/comparator/PinyinComparator.java new file mode 100644 index 000000000..92ce23f27 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/comparator/PropertyComparator.java b/hutool-core/src/main/java/cn/hutool/core/comparator/PropertyComparator.java new file mode 100644 index 000000000..a6e0a4648 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/comparator/PropertyComparator.java @@ -0,0 +1,74 @@ +package cn.hutool.core.comparator; + +import java.io.Serializable; +import java.util.Comparator; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.ObjectUtil; + +/** + * Bean属性排序器
+ * 支持读取Bean多层次下的属性 + * + * @author Looly + * + * @param 被比较的Bean + */ +public class PropertyComparator implements Comparator, Serializable { + private static final long serialVersionUID = 9157326766723846313L; + + private final String property; + private final boolean isNullGreater; + + /** + * 构造 + * + * @param property 属性名 + */ + public PropertyComparator(String property) { + this(property, true); + } + + /** + * 构造 + * + * @param property 属性名 + * @param isNullGreater null值是否排在后(从小到大排序) + */ + public PropertyComparator(String property, boolean isNullGreater) { + this.property = property; + this.isNullGreater = isNullGreater; + } + + @Override + public int compare(T o1, T o2) { + if (o1 == o2) { + return 0; + } else if (null == o1) {// null 排在后面 + return isNullGreater ? 1 : -1; + } else if (null == o2) { + return isNullGreater ? -1 : 1; + } + + Comparable v1; + Comparable v2; + try { + v1 = (Comparable) BeanUtil.getProperty(o1, property); + v2 = (Comparable) BeanUtil.getProperty(o2, property); + } 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, isNullGreater); + if(0 == result) { + //避免TreeSet / TreeMap 过滤掉排序字段相同但是对象不相同的情况 + result = CompareUtil.compare(o1, o2, this.isNullGreater); + } + return result; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/comparator/ReverseComparator.java b/hutool-core/src/main/java/cn/hutool/core/comparator/ReverseComparator.java new file mode 100644 index 000000000..5c9a3dda3 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/comparator/VersionComparator.java b/hutool-core/src/main/java/cn/hutool/core/comparator/VersionComparator.java new file mode 100644 index 000000000..e21dae51a --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/comparator/VersionComparator.java @@ -0,0 +1,102 @@ +package cn.hutool.core.comparator; + +import java.io.Serializable; +import java.util.Comparator; +import java.util.List; + +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 版本比较器
+ * 比较两个版本的大小
+ * 排序时版本从小到大排序,既比较时小版本在前,大版本在后
+ * 支持如: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(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(); + } + + @Override + public boolean equals(final Object object) { + if (this == object) { + return true; + } + if (null == object) { + return false; + } + if (object.getClass().equals(this.getClass())) { + final VersionComparator other = (VersionComparator) object; + return this.equals(other); + } + return false; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/comparator/package-info.java b/hutool-core/src/main/java/cn/hutool/core/comparator/package-info.java new file mode 100644 index 000000000..bda472fa2 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/convert/AbstractConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/AbstractConverter.java new file mode 100644 index 000000000..a947f127d --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/convert/AbstractConverter.java @@ -0,0 +1,116 @@ +package cn.hutool.core.convert; + +import java.io.Serializable; +import java.util.Map; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 抽象转换器,提供通用的转换逻辑,同时通过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) && false == Map.class.isAssignableFrom(targetType)) { + // 除Map外,已经是目标类型,不需要转换(Map类型涉及参数类型,需要单独转换) + return (T) targetType.cast(value); + } + T result = convertInternal(value); + return ((null == result) ? defaultValue : result); + } else { + throw new IllegalArgumentException(StrUtil.format("Default value [{}] is not the instance of [{}]", defaultValue, 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/hutool-core/src/main/java/cn/hutool/core/convert/BasicType.java b/hutool-core/src/main/java/cn/hutool/core/convert/BasicType.java new file mode 100644 index 000000000..ff9b1837c --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/convert/BasicType.java @@ -0,0 +1,59 @@ +package cn.hutool.core.convert; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 基本变量类型的枚举
+ * 基本类型枚举包括原始类型和包装类型 + * @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> wrapperPrimitiveMap = new ConcurrentHashMap<>(8); + /** 原始类型为Key,包装类型为Value,例如: int.class =》 Integer.class. */ + public static final Map, Class> primitiveWrapperMap = new ConcurrentHashMap<>(8); + + static { + wrapperPrimitiveMap.put(Boolean.class, boolean.class); + wrapperPrimitiveMap.put(Byte.class, byte.class); + wrapperPrimitiveMap.put(Character.class, char.class); + wrapperPrimitiveMap.put(Double.class, double.class); + wrapperPrimitiveMap.put(Float.class, float.class); + wrapperPrimitiveMap.put(Integer.class, int.class); + wrapperPrimitiveMap.put(Long.class, long.class); + wrapperPrimitiveMap.put(Short.class, short.class); + + for (Map.Entry, Class> entry : wrapperPrimitiveMap.entrySet()) { + primitiveWrapperMap.put(entry.getValue(), entry.getKey()); + } + } + + /** + * 原始类转为包装类,非原始类返回原类 + * @param clazz 原始类 + * @return 包装类 + */ + public static Class wrap(Class clazz){ + if(null == clazz || false == clazz.isPrimitive()){ + return clazz; + } + Class result = primitiveWrapperMap.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 = wrapperPrimitiveMap.get(clazz); + return (null == result) ? clazz : result; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/Convert.java b/hutool-core/src/main/java/cn/hutool/core/convert/Convert.java new file mode 100644 index 000000000..63c0a5668 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/convert/Convert.java @@ -0,0 +1,1014 @@ +package cn.hutool.core.convert; + +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import cn.hutool.core.convert.impl.CollectionConverter; +import cn.hutool.core.convert.impl.GenericEnumConverter; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.TypeReference; +import cn.hutool.core.text.UnicodeUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.HexUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 类型转换器 + * + * @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); + } + + /** + * 转换为字符串
+ * 如果给定的值为null,或者转换失败,返回默认值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); + } + + /** + * 转换为字符
+ * 如果给定的值为null,或者转换失败,返回默认值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
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Byte toByte(Object value, Byte defaultValue) { + return convertQuietly(Byte.class, value, defaultValue); + } + + /** + * 转换为byte
+ * 如果给定的值为null,或者转换失败,返回默认值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); + } + + /** + * 转换为Short
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Short toShort(Object value, Short defaultValue) { + return convertQuietly(Short.class, value, defaultValue); + } + + /** + * 转换为Short
+ * 如果给定的值为null,或者转换失败,返回默认值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
+ * 如果给定的值为空,或者转换失败,返回默认值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
+ * 如果给定的值为null,或者转换失败,返回默认值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
+ * 如果给定的值为null,或者转换失败,返回默认值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
+ * 如果给定的值为空,或者转换失败,返回默认值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
+ * 如果给定的值为空,或者转换失败,返回默认值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
+ * 如果给定的值为空,或者转换失败,返回默认值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
+ * 如果给定的值为空,或者转换失败,返回默认值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); + } + + /** + * 转换为Date
+ * 如果给定的值为空,或者转换失败,返回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 + */ + public static > E toEnum(Class clazz, Object value, E defaultValue) { + return (new GenericEnumConverter<>(clazz)).convertQuietly(value, defaultValue); + } + + /** + * 转换为Enum对象
+ * 如果给定的值为空,或者转换失败,返回默认值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 List} + * @since 4.1.20 + */ + @SuppressWarnings("unchecked") + public static List toList(Class elementType, Object value) { + return (List) toCollection(ArrayList.class, elementType, value); + } + + /** + * 转换值为指定类型,类型采用字符串表示 + * + * @param 目标类型 + * @param className 类的字符串表示 + * @param value 值 + * @return 转换后的值 + * @since 4.0.7 + * @throws ConvertException 转换器不存在 + */ + @SuppressWarnings("unchecked") + public static T convertByClassName(String className, Object value) throws ConvertException{ + return (T) convert(ClassUtil.loadClass(className), value); + } + + /** + * 转换值为指定类型 + * + * @param 目标类型 + * @param type 类型 + * @param value 值 + * @return 转换后的值 + * @since 4.0.0 + * @throws ConvertException 转换器不存在 + */ + 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 ConverterRegistry.getInstance().convert(type, value, defaultValue); + } + + /** + * 转换值为指定类型,不抛异常转换
+ * 当转换失败时返回{@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) { + try { + return convert(type, value, defaultValue); + } catch (Exception e) { + return defaultValue; + } + } + + // ----------------------------------------------------------------------- 全角半角转换 + /** + * 半角转全角 + * + * @param input String. + * @return 全角字符串. + */ + public static String toSBC(String input) { + return toSBC(input, null); + } + + /** + * 半角转全角 + * + * @param input String + * @param notConvertSet 不替换的字符集合 + * @return 全角字符串. + */ + public static String toSBC(String input, Set notConvertSet) { + char c[] = input.toCharArray(); + for (int i = 0; i < c.length; i++) { + if (null != notConvertSet && notConvertSet.contains(c[i])) { + // 跳过不替换的字符 + continue; + } + + if (c[i] == ' ') { + 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; + } + 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); + } + } + String returnString = new String(c); + + return returnString; + } + + // --------------------------------------------------------------------- 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) + * @deprecated 请使用 {@link #hexToStr(String, Charset)} + */ + @Deprecated + public static String hexStrToStr(String hexStr, Charset charset) { + return hexToStr(hexStr, charset); + } + + /** + * 十六进制转换字符串 + * + * @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); + } + + // --------------------------------------------------------------- 原始包装类型转换 + /** + * 原始类转为包装类,非原始类返回原类 + * + * @see BasicType#wrap(Class) + * @param clazz 原始类 + * @return 包装类 + * @see BasicType#wrap(Class) + */ + public static Class wrap(Class clazz) { + return BasicType.wrap(clazz); + } + + /** + * 包装类转为原始类,非包装类返回原类 + * + * @see BasicType#unWrap(Class) + * @param clazz 包装类 + * @return 原始类 + * @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 NumberWordFormater.format(number); + } + + /** + * 将阿拉伯数字转为中文表达方式 + * + * @param number 数字 + * @param isUseTraditonal 是否使用繁体字(金额形式) + * @return 中文 + * @since 3.2.3 + */ + public static String numberToChinese(double number, boolean isUseTraditonal) { + return NumberChineseFormater.format(number, isUseTraditonal); + } + + /** + * 金额转为中文形式 + * + * @param n 数字 + * @return 中文大写数字 + * @since 3.2.3 + */ + public static String digitToChinese(Number n) { + if(null == n) { + return "零"; + } + return NumberChineseFormater.format(n.doubleValue(), true, true); + } + + // -------------------------------------------------------------------------- 数字转换 + /** + * 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 3.2.0 + */ + public static short bytesToShort(byte[] bytes) { + return (short) (bytes[1] & 0xff | (bytes[0] & 0xff) << 8); + } + + /** + * short转byte数组 + * @param shortValue short值 + * @return byte数组 + * @since 3.2.0 + */ + public static byte[] shortToBytes(short shortValue) { + byte[] b = new byte[2]; + b[1] = (byte) (shortValue & 0xff); + b[0] = (byte) ((shortValue >> 8) & 0xff); + return b; + } + + /** + * byte[]转int值 + * + * @param bytes byte数组 + * @return int值 + * @since 3.2.0 + */ + public static int bytesToInt(byte[] bytes) { + return bytes[3] & 0xFF | // + (bytes[2] & 0xFF) << 8 | // + (bytes[1] & 0xFF) << 16 | // + (bytes[0] & 0xFF) << 24; // + } + + /** + * int转byte数组 + * + * @param intValue int值 + * @return byte数组 + * @since 3.2.0 + */ + public static byte[] intToBytes(int intValue) { + 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数组 + * @since 3.2.0 + */ + public static byte[] longToBytes(long longValue) { + // Magic number 8 should be defined as Long.SIZE / Byte.SIZE + final byte[] result = new byte[8]; + for (int i = 7; i >= 0; i--) { + result[i] = (byte) (longValue & 0xFF); + longValue >>= 8; + } + 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值 + * @since 3.2.0 + */ + public static long bytesToLong(byte[] bytes) { + // Magic number 8 should be defined as Long.SIZE / Byte.SIZE + long values = 0; + for (int i = 0; i < 8; i++) { + values <<= 8; + values |= (bytes[i] & 0xff); + } + return values; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/ConvertException.java b/hutool-core/src/main/java/cn/hutool/core/convert/ConvertException.java new file mode 100644 index 000000000..822695607 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/convert/Converter.java b/hutool-core/src/main/java/cn/hutool/core/convert/Converter.java new file mode 100644 index 000000000..982c23ee6 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/convert/Converter.java @@ -0,0 +1,22 @@ +package cn.hutool.core.convert; + +/** + * 转换器接口,实现类型转换 + * + * @param 转换到的目标类型 + * @author Looly + */ +public interface Converter { + + /** + * 转换为指定类型
+ * 如果类型无法确定,将读取默认值的类型做为目标类型 + * + * @param value 原始值 + * @param defaultValue 默认值 + * @return 转换后的值 + * @throws IllegalArgumentException 无法确定目标类型,且默认值为{@code null},无法确定类型 + */ + public T convert(Object value, T defaultValue) throws IllegalArgumentException; + +} \ No newline at end of file diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/ConverterRegistry.java b/hutool-core/src/main/java/cn/hutool/core/convert/ConverterRegistry.java new file mode 100644 index 000000000..2bb90dd11 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/convert/ConverterRegistry.java @@ -0,0 +1,399 @@ +package cn.hutool.core.convert; + +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.util.Calendar; +import java.util.Collection; +import java.util.Currency; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.convert.impl.ArrayConverter; +import cn.hutool.core.convert.impl.AtomicBooleanConverter; +import cn.hutool.core.convert.impl.AtomicReferenceConverter; +import cn.hutool.core.convert.impl.BeanConverter; +import cn.hutool.core.convert.impl.BooleanConverter; +import cn.hutool.core.convert.impl.CalendarConverter; +import cn.hutool.core.convert.impl.CharacterConverter; +import cn.hutool.core.convert.impl.CharsetConverter; +import cn.hutool.core.convert.impl.ClassConverter; +import cn.hutool.core.convert.impl.CollectionConverter; +import cn.hutool.core.convert.impl.CurrencyConverter; +import cn.hutool.core.convert.impl.DateConverter; +import cn.hutool.core.convert.impl.EnumConverter; +import cn.hutool.core.convert.impl.Jdk8DateConverter; +import cn.hutool.core.convert.impl.LocaleConverter; +import cn.hutool.core.convert.impl.MapConverter; +import cn.hutool.core.convert.impl.NumberConverter; +import cn.hutool.core.convert.impl.PathConverter; +import cn.hutool.core.convert.impl.PrimitiveConverter; +import cn.hutool.core.convert.impl.ReferenceConverter; +import cn.hutool.core.convert.impl.StackTraceElementConverter; +import cn.hutool.core.convert.impl.StringConverter; +import cn.hutool.core.convert.impl.TimeZoneConverter; +import cn.hutool.core.convert.impl.URIConverter; +import cn.hutool.core.convert.impl.URLConverter; +import cn.hutool.core.convert.impl.UUIDConverter; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.TypeUtil; + +/** + * 转换器登记中心 + *

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

+ *

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

+ * + * @author Looly + * + */ +public class ConverterRegistry implements Serializable{ + private static final long serialVersionUID = 1L; + + /** 默认类型转换器 */ + private Map> defaultConverterMap; + /** 用户自定义类型转换器 */ + private Map> customConverterMap; + + /** 类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例 没有绑定关系,而且只有被调用到才会装载,从而实现了延迟加载 */ + private static class SingletonHolder { + /** 静态初始化器,由JVM来保证线程安全 */ + private static ConverterRegistry instance = new ConverterRegistry(); + } + + /** + * 获得单例的 {@link ConverterRegistry} + * + * @return {@link ConverterRegistry} + */ + public static ConverterRegistry getInstance() { + return SingletonHolder.instance; + } + + public ConverterRegistry() { + defaultConverter(); + } + + /** + * 登记自定义转换器 + * + * @param type 转换的目标类型 + * @param converterClass 转换器类,必须有默认构造方法 + * @return {@link ConverterRegistry} + */ + public ConverterRegistry putCustom(Type type, Class> converterClass) { + return putCustom(type, ReflectUtil.newInstance(converterClass)); + } + + /** + * 登记自定义转换器 + * + * @param type 转换的目标类型 + * @param converter 转换器 + * @return {@link ConverterRegistry} + */ + public ConverterRegistry putCustom(Type type, Converter converter) { + if (null == customConverterMap) { + synchronized (this) { + if (null == customConverterMap) { + customConverterMap = new ConcurrentHashMap<>(); + } + } + } + customConverterMap.put(type, converter); + return this; + } + + /** + * 获得转换器
+ * + * @param 转换的目标类型 + * + * @param type 类型 + * @param isCustomFirst 是否自定义转换器优先 + * @return 转换器 + */ + public Converter getConverter(Type type, boolean isCustomFirst) { + Converter converter = null; + 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.isUnknow(type) && null == defaultValue) { + // 对于用户不指定目标类型的情况,返回原值 + return (T) value; + } + if (ObjectUtil.isNull(value)) { + return defaultValue; + } + if (TypeUtil.isUnknow(type)) { + type = defaultValue.getClass(); + } + + // 标准转换器 + 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("No Converter for type [{}]", rowType.getName()); + } + + /** + * 转换值为指定类型
+ * 自定义转换器优先 + * + * @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 转换后的值,默认为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.isArray()) { + final ArrayConverter arrayConverter = new ArrayConverter(rowType); + try { + return (T) arrayConverter.convert(value, defaultValue); + } catch (Exception e) { + // 数组转换失败进行下一步 + } + } + + // 枚举转换 + if (rowType.isEnum()) { + return (T) new EnumConverter(rowType).convert(value, defaultValue); + } + + // 表示非需要特殊转换的对象 + return null; + } + + /** + * 注册默认转换器 + * + * @return 转换器 + */ + private ConverterRegistry defaultConverter() { + defaultConverterMap = new ConcurrentHashMap<>(); + + // 原始类型转换器 + 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(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(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)); + defaultConverterMap.put(java.sql.Date.class, new DateConverter(java.sql.Date.class)); + defaultConverterMap.put(java.sql.Time.class, new DateConverter(java.sql.Time.class)); + defaultConverterMap.put(java.sql.Timestamp.class, new DateConverter(java.sql.Timestamp.class)); + + // 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 + + // 其它类型 + 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 + + // JDK8+ + try { + Class clazz; + for (String className : Jdk8DateConverter.supportClassNames) { + clazz = ClassUtil.loadClass(className); + defaultConverterMap.put(clazz, new Jdk8DateConverter(clazz));// since 4.5.1 + } + } catch (Exception e) { + // ignore + // 在使用jdk8以下版本时,其转换器自动跳过失效 + } + + return this; + } + // ----------------------------------------------------------- Private method end +} diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/NumberChineseFormater.java b/hutool-core/src/main/java/cn/hutool/core/convert/NumberChineseFormater.java new file mode 100644 index 000000000..2cbe043f3 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/convert/NumberChineseFormater.java @@ -0,0 +1,172 @@ +package cn.hutool.core.convert; + +import cn.hutool.core.util.StrUtil; + +/** + * 数字转中文类
+ * 包括: + *
+ * 1. 数字转中文大写形式,比如一百二十一
+ * 2. 数字转金额用的大写形式,比如:壹佰贰拾壹
+ * 3. 转金额形式,比如:壹佰贰拾壹整
+ * 
+ * + * @author fanqun,looly + **/ +public class NumberChineseFormater { + + /** 简体中文形式 **/ + private static final String[] simpleDigits = { "零", "一", "二", "三", "四", "五", "六", "七", "八", "九" }; + /** 繁体中文形式 **/ + private static final String[] traditionalDigits = { "零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖" }; + + /** 简体中文单位 **/ + private static final String[] simpleUnits = { "", "十", "百", "千" }; + /** 繁体中文单位 **/ + private static final String[] traditionalUnits = { "", "拾", "佰", "仟" }; + + /** + * 阿拉伯数字转换成中文,小数点后四舍五入保留两位. 使用于整数、小数的转换. + * + * @param amount 数字 + * @param isUseTraditional 是否使用繁体 + * @return 中文 + */ + public static String format(double amount, boolean isUseTraditional) { + return format(amount, isUseTraditional, false); + } + + /** + * 阿拉伯数字转换成中文,小数点后四舍五入保留两位. 使用于整数、小数的转换. + * + * @param amount 数字 + * @param isUseTraditional 是否使用繁体 + * @param isMoneyMode 是否为金额模式 + * @return 中文 + */ + public static String format(double amount, boolean isUseTraditional, boolean isMoneyMode) { + final String[] numArray = isUseTraditional ? traditionalDigits : simpleDigits; + + if (amount > 99999999999999.99 || amount < -99999999999999.99) { + throw new IllegalArgumentException("Number support only: (-99999999999999.99 ~ 99999999999999.99)!"); + } + + boolean negative = false; + if (amount < 0) { + negative = true; + amount = - amount; + } + + long temp = Math.round(amount * 100); + int numFen = (int) (temp % 10); + temp = temp / 10; + int numJiao = (int) (temp % 10); + temp = temp / 10; + + //将数字以万为单位分为多份 + int[] parts = new int[20]; + int numParts = 0; + for (int i = 0; temp != 0; i++) { + int part = (int) (temp % 10000); + parts[i] = part; + numParts++; + temp = temp / 10000; + } + + boolean beforeWanIsZero = true; // 标志“万”下面一级是不是 0 + + String chineseStr = StrUtil.EMPTY; + for (int i = 0; i < numParts; i++) { + String partChinese = toChinese(parts[i], isUseTraditional); + if (i % 2 == 0) { + beforeWanIsZero = StrUtil.isEmpty(partChinese); + } + + if (i != 0) { + if (i % 2 == 0) { + chineseStr = "亿" + chineseStr; + } else { + if ("".equals(partChinese) && false == beforeWanIsZero) { + // 如果“万”对应的 part 为 0,而“万”下面一级不为 0,则不加“万”,而加“零” + chineseStr = "零" + chineseStr; + } else { + if (parts[i - 1] < 1000 && parts[i - 1] > 0) { + // 如果"万"的部分不为 0, 而"万"前面的部分小于 1000 大于 0, 则万后面应该跟“零” + chineseStr = "零" + chineseStr; + } + chineseStr = "万" + chineseStr; + } + } + } + chineseStr = partChinese + chineseStr; + } + + // 整数部分为 0, 则表达为"零" + if (StrUtil.EMPTY.equals(chineseStr)) { + chineseStr = numArray[0]; + } + //负数 + if (negative) { // 整数部分不为 0 + chineseStr = "负" + chineseStr; + } + + // 小数部分 + if (numFen != 0 || numJiao != 0) { + if (numFen == 0) { + chineseStr += (isMoneyMode ? "元" : "点") + numArray[numJiao] + (isMoneyMode ? "角" : ""); + } else { // “分”数不为 0 + if (numJiao == 0) { + chineseStr += (isMoneyMode ? "元零" : "点零") + numArray[numFen] + (isMoneyMode ? "分" : ""); + } else { + chineseStr += (isMoneyMode ? "元" : "点") + numArray[numJiao] + (isMoneyMode ? "角" : "") + numArray[numFen] + (isMoneyMode ? "分" : ""); + } + } + }else if(isMoneyMode) { + //无小数部分的金额结尾 + chineseStr += "元整"; + } + + return chineseStr; + + } + + /** + * 把一个 0~9999 之间的整数转换为汉字的字符串,如果是 0 则返回 "" + * + * @param amountPart 数字部分 + * @param isUseTraditional 是否使用繁体单位 + * @return 转换后的汉字 + */ + private static String toChinese(int amountPart, boolean isUseTraditional) { +// if (amountPart < 0 || amountPart > 10000) { +// throw new IllegalArgumentException("Number must 0 < num < 10000!"); +// } + + String[] numArray = isUseTraditional ? traditionalDigits : simpleDigits; + String[] units = isUseTraditional ? traditionalUnits : simpleUnits; + + int temp = amountPart; + + String chineseStr = ""; + boolean lastIsZero = true; // 在从低位往高位循环时,记录上一位数字是不是 0 + for (int i = 0; temp > 0; i++) { + if (temp == 0) { + // 高位已无数据 + break; + } + int digit = temp % 10; + if (digit == 0) { // 取到的数字为 0 + if (false == lastIsZero) { + // 前一个数字不是 0,则在当前汉字串前加“零”字; + chineseStr = "零" + chineseStr; + } + lastIsZero = true; + } else { // 取到的数字不是 0 + chineseStr = numArray[digit] + units[i] + chineseStr; + lastIsZero = false; + } + temp = temp / 10; + } + return chineseStr; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/NumberWordFormater.java b/hutool-core/src/main/java/cn/hutool/core/convert/NumberWordFormater.java new file mode 100644 index 000000000..708e85859 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/convert/NumberWordFormater.java @@ -0,0 +1,143 @@ +package cn.hutool.core.convert; + +import cn.hutool.core.util.StrUtil; + +/** + * 将浮点数类型的number转换成英语的表达方式
+ * 参考博客:http://blog.csdn.net/eric_sunah/article/details/8713226 + * + * @author Looly + * @since 3.0.9 + * @see http://blog.csdn.net/eric_sunah/article/details/8713226 + */ +public class NumberWordFormater { + + 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", "TWELEVE", "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" }; + + /** + * 将阿拉伯数字转为英文表达式 + * + * @param x + * 阿拉伯数字,可以为{@link Number}对象,也可以是普通对象,最后会使用字符串方式处理 + * @return 英文表达式 + */ + public static String format(Object x) { + if (x != null) { + return format(x.toString()); + } else { + return ""; + } + } + + /** + * 将阿拉伯数字转为英文表达式 + * + * @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; + } + String lm = ""; // 用来存放转换後的整数部分 + for (int i = 0; i < lstrrev.length() / 3; i++) { + a[i] = StrUtil.reverse(lstrrev.substring(3 * i, 3 * i + 3)); // 截取第一个叁位 + if (!a[i].equals("000")) { // 用来避免这种情况:1000000 = one million + // thousand only + if (i != 0) { + lm = transThree(a[i]) + " " + parseMore(String.valueOf(i)) + " " + lm; // 加: + // thousand、million、billion + } else { + lm = transThree(a[i]); // 防止i=0时, 在多加两个空格. + } + } else { + lm += transThree(a[i]); + } + } + + String xs = ""; // 用来存放转换後小数部分 + if (z > -1) { + xs = "AND CENTS " + transTwo(rstr) + " "; // 小数部分存在时转换小数 + } + + return lm.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(String s) { + return NUMBER_MORE[Integer.parseInt(s)]; + } + + // 两位 + 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 (s.substring(1).equals("00")) {// 是否被100整除 + value = parseFirst(s.substring(0, 1)) + " HUNDRED"; + } else { + value = parseFirst(s.substring(0, 1)) + " HUNDRED AND " + transTwo(s.substring(1)); + } + return value; + } +} \ No newline at end of file diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/impl/ArrayConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/ArrayConverter.java new file mode 100644 index 000000000..b26dad856 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/convert/impl/ArrayConverter.java @@ -0,0 +1,152 @@ +package cn.hutool.core.convert.impl; + +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import cn.hutool.core.collection.IterUtil; +import cn.hutool.core.convert.AbstractConverter; +import cn.hutool.core.convert.ConverterRegistry; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 数组转换器,包括原始类型数组 + * + * @author Looly + */ +public class ArrayConverter extends AbstractConverter { + private static final long serialVersionUID = 1L; + + private final Class targetType; + /** 目标元素类型 */ + private final Class targetComponentType; + + /** + * 构造 + * + * @param targetType 目标数组类型 + */ + public ArrayConverter(Class targetType) { + 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); + } + } + + @Override + protected Object convertInternal(Object value) { + return value.getClass().isArray() ? convertArrayToArray(value) : convertObjectToArray(value); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public Class getTargetType() { + return this.targetType; + } + + // -------------------------------------------------------------------------------------- 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); + + final ConverterRegistry converter = ConverterRegistry.getInstance(); + for (int i = 0; i < len; i++) { + Array.set(result, i, converter.convert(targetComponentType, 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()); + } + + // 单纯字符串情况下按照逗号分隔后劈开 + final String[] strings = StrUtil.split(value.toString(), StrUtil.COMMA); + return convertArrayToArray(strings); + } + + final ConverterRegistry converter = ConverterRegistry.getInstance(); + Object result = null; + 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, converter.convert(targetComponentType, 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, converter.convert(targetComponentType, 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, converter.convert(targetComponentType, 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, converter.convert(targetComponentType, list.get(i))); + } + } 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] = ConverterRegistry.getInstance().convert(targetComponentType, value); + return singleElementArray; + } + // -------------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/impl/AtomicBooleanConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/AtomicBooleanConverter.java new file mode 100644 index 000000000..1cbd873bc --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/convert/impl/AtomicBooleanConverter.java @@ -0,0 +1,29 @@ +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 (boolean.class == value.getClass()) { + return new AtomicBoolean((boolean) value); + } + if (value instanceof Boolean) { + return new AtomicBoolean((Boolean) value); + } + final String valueStr = convertToStr(value); + return new AtomicBoolean(BooleanUtil.toBoolean(valueStr)); + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/impl/AtomicReferenceConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/AtomicReferenceConverter.java new file mode 100644 index 000000000..7e8fabaa8 --- /dev/null +++ b/hutool-core/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(false == TypeUtil.isUnknow(paramType)){ + targetValue = ConverterRegistry.getInstance().convert(paramType, value); + } + if(null == targetValue){ + targetValue = value; + } + + return new AtomicReference<>(targetValue); + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/impl/BeanConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/BeanConverter.java new file mode 100644 index 000000000..91d0de532 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/convert/impl/BeanConverter.java @@ -0,0 +1,83 @@ +package cn.hutool.core.convert.impl; + +import java.lang.reflect.Type; +import java.util.Map; + +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.map.MapProxy; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.TypeUtil; + +/** + * 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 Type beanType; + private Class beanClass; + private 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(); + } + return null; + } + + @Override + public Class getTargetType() { + return this.beanClass; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/impl/BooleanConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/BooleanConverter.java new file mode 100644 index 000000000..a205369cd --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/convert/impl/BooleanConverter.java @@ -0,0 +1,23 @@ +package cn.hutool.core.convert.impl; + +import cn.hutool.core.convert.AbstractConverter; +import cn.hutool.core.util.BooleanUtil; + +/** + * 波尔转换器 + * @author Looly + * + */ +public class BooleanConverter extends AbstractConverter{ + private static final long serialVersionUID = 1L; + + @Override + protected Boolean convertInternal(Object value) { + if(boolean.class == value.getClass()){ + return Boolean.valueOf((boolean)value); + } + String valueStr = convertToStr(value); + return Boolean.valueOf(BooleanUtil.toBoolean(valueStr)); + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/impl/CalendarConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/CalendarConverter.java new file mode 100644 index 000000000..4a2653cde --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/convert/impl/CastConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/CastConverter.java new file mode 100644 index 000000000..12e8e6736 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/convert/impl/CharacterConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/CharacterConverter.java new file mode 100644 index 000000000..a641aa7f1 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/convert/impl/CharacterConverter.java @@ -0,0 +1,33 @@ +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 (char.class == value.getClass()) { + return Character.valueOf((char) value); + } else if (value instanceof Boolean) { + return BooleanUtil.toCharacter((Boolean) value); + } else if (boolean.class == value.getClass()) { + return BooleanUtil.toCharacter((boolean) value); + } else { + final String valueStr = convertToStr(value); + if (StrUtil.isNotBlank(valueStr)) { + return Character.valueOf(valueStr.charAt(0)); + } + } + return null; + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/impl/CharsetConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/CharsetConverter.java new file mode 100644 index 000000000..99f29475b --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/convert/impl/ClassConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/ClassConverter.java new file mode 100644 index 000000000..d2e1ec647 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/convert/impl/ClassConverter.java @@ -0,0 +1,26 @@ +package cn.hutool.core.convert.impl; + +import cn.hutool.core.convert.AbstractConverter; +import cn.hutool.core.util.ClassUtil; + +/** + * 类转换器
+ * 将类名转换为类 + * @author Looly + * + */ +public class ClassConverter extends AbstractConverter>{ + private static final long serialVersionUID = 1L; + + @Override + protected Class convertInternal(Object value) { + String valueStr = convertToStr(value); + try { + return ClassUtil.getClassLoader().loadClass(valueStr); + } catch (Exception e) { + // Ignore Exception + } + return null; + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/impl/CollectionConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/CollectionConverter.java new file mode 100644 index 000000000..ad2cde315 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/convert/impl/CollectionConverter.java @@ -0,0 +1,83 @@ +package cn.hutool.core.convert.impl; + +import java.lang.reflect.Type; +import java.util.Collection; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.convert.Converter; +import cn.hutool.core.util.TypeUtil; + +/** + * 各种集合类转换器 + * + * @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 { + Collection result = null; + try { + result = convertInternal(value); + } catch (RuntimeException e) { + return defaultValue; + } + return ((null == result) ? defaultValue : result); + } + + /** + * 内部转换 + * + * @param value 值 + * @return 转换后的集合对象 + */ + protected Collection convertInternal(Object value) { + final Collection collection = CollectionUtil.create(TypeUtil.getClass(this.collectionType)); + return CollUtil.addAll(collection, value, this.elementType); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/impl/CurrencyConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/CurrencyConverter.java new file mode 100644 index 000000000..784733b43 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/convert/impl/DateConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/DateConverter.java new file mode 100644 index 000000000..011aa898b --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/convert/impl/DateConverter.java @@ -0,0 +1,101 @@ +package cn.hutool.core.convert.impl; + +import java.util.Calendar; + +import cn.hutool.core.convert.AbstractConverter; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 日期转换器 + * + * @author Looly + * + */ +public class DateConverter extends AbstractConverter { + private static final long serialVersionUID = 1L; + + private 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) { + Long mills = null; + if (value instanceof Calendar) { + // Handle Calendar + mills = ((Calendar) value).getTimeInMillis(); + } else if (value instanceof Long) { + // Handle Long + mills = (Long) value; + } else { + // 统一按照字符串处理 + final String valueStr = convertToStr(value); + try { + mills = StrUtil.isBlank(this.format) ? DateUtil.parse(valueStr).getTime() : DateUtil.parse(valueStr, this.format).getTime(); + } catch (Exception e) { + // Ignore Exception + } + } + + if (null == mills) { + return null; + } + + // 返回指定类型 + if (java.util.Date.class == targetType) { + return new java.util.Date(mills); + } + if (DateTime.class == targetType) { + return new DateTime(mills); + } else if (java.sql.Date.class == targetType) { + return new java.sql.Date(mills); + } else if (java.sql.Time.class == targetType) { + return new java.sql.Time(mills); + } else if (java.sql.Timestamp.class == targetType) { + return new java.sql.Timestamp(mills); + } + + throw new UnsupportedOperationException(StrUtil.format("Unsupport Date type: {}", this.targetType.getName())); + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/impl/EnumConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/EnumConverter.java new file mode 100644 index 000000000..3e9f26165 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/convert/impl/EnumConverter.java @@ -0,0 +1,35 @@ +package cn.hutool.core.convert.impl; + +import cn.hutool.core.convert.AbstractConverter; + +/** + * 无泛型检查的枚举转换器 + * + * @author Looly + * @since 4.0.2 + */ +@SuppressWarnings({ "unchecked", "rawtypes" }) +public class EnumConverter extends AbstractConverter { + private static final long serialVersionUID = 1L; + + private Class enumClass; + + /** + * 构造 + * + * @param enumClass 转换成的目标Enum类 + */ + public EnumConverter(Class enumClass) { + this.enumClass = enumClass; + } + + @Override + protected Object convertInternal(Object value) { + return Enum.valueOf(enumClass, convertToStr(value)); + } + + @Override + public Class getTargetType() { + return this.enumClass; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/impl/GenericEnumConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/GenericEnumConverter.java new file mode 100644 index 000000000..36676fa3b --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/convert/impl/GenericEnumConverter.java @@ -0,0 +1,35 @@ +package cn.hutool.core.convert.impl; + +import cn.hutool.core.convert.AbstractConverter; + +/** + * 泛型枚举转换器 + * + * @param 枚举类类型 + * @author Looly + * @since 4.0.2 + */ +public class GenericEnumConverter> extends AbstractConverter { + private static final long serialVersionUID = 1L; + + private Class enumClass; + + /** + * 构造 + * + * @param enumClass 转换成的目标Enum类 + */ + public GenericEnumConverter(Class enumClass) { + this.enumClass = enumClass; + } + + @Override + protected E convertInternal(Object value) { + return Enum.valueOf(enumClass, convertToStr(value)); + } + + @Override + public Class getTargetType() { + return this.enumClass; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/impl/Jdk8DateConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/Jdk8DateConverter.java new file mode 100644 index 000000000..528303306 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/convert/impl/Jdk8DateConverter.java @@ -0,0 +1,152 @@ +package cn.hutool.core.convert.impl; + +import java.lang.reflect.Method; + +import cn.hutool.core.convert.AbstractConverter; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ReflectUtil; + +/** + * JDK8中新加入的java.time包对象解析转换器
+ * 通过反射调用“parse方法”,支持的对象包括: + * + *
+ * java.time.LocalDateTime
+ * java.time.LocalDate
+ * java.time.LocalTime
+ * java.time.ZonedDateTime
+ * java.time.OffsetDateTime
+ * java.time.OffsetTime
+ * java.time.Period
+ * java.time.Instant
+ * 
+ * + * @author looly + * + */ +public class Jdk8DateConverter extends AbstractConverter { + private static final long serialVersionUID = 1L; + + /** 支持的JDK中的类名 */ + public static String[] supportClassNames = new String[] { // + "java.time.LocalDateTime", // + "java.time.LocalDate", // + "java.time.LocalTime", // + "java.time.ZonedDateTime", // + "java.time.OffsetDateTime", // + "java.time.OffsetTime", // + "java.time.Period", // + "java.time.Instant"// + }; + + private Class targetType; + /** 日期格式化 */ + private String format; + + /** + * 构造 + * + * @param targetType 目标类型 + */ + public Jdk8DateConverter(Class targetType) { + this.targetType = targetType; + } + + /** + * 构造 + * + * @param targetType 目标类型 + * @param format 日期格式 + */ + public Jdk8DateConverter(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 Object convertInternal(Object value) { + if (value instanceof Long) { + return parseFromLong((Long) value); + } else { + return parseFromCharSequence(convertToStr(value)); + } + } + + /** + * 通过反射从字符串转java.time中的对象 + * + * @param value 字符串值 + * @return 日期对象 + */ + private Object parseFromCharSequence(CharSequence value) { + Method method; + if (null != this.format) { + final Object dateTimeFormatter = getDateTimeFormatter(); + method = ReflectUtil.getMethod(this.targetType, "parse", CharSequence.class, dateTimeFormatter.getClass()); + return ReflectUtil.invokeStatic(method, value, dateTimeFormatter); + } else { + method = ReflectUtil.getMethod(this.targetType, "parse", CharSequence.class); + return ReflectUtil.invokeStatic(method, value); + } + } + + /** + * 通过反射将Long型时间戳转换为java.time中的对象 + * + * @param time 时间戳 + * @return java.time中的对象 + */ + private Object parseFromLong(Long time) { + String targetName = this.targetType.getName(); + if ("java.time.Instant".equals(targetName)) { + return toInstant(time); + } + return null; + } + + /** + * 反射获取java.time.format.DateTimeFormatter对象 + * + * @return java.time.format.DateTimeFormatter对象 + */ + private Object getDateTimeFormatter() { + if (null != this.format) { + return ClassUtil.invoke("java.time.format.DateTimeFormatter.ofPattern", false, this.format); + } + return null; + } + + /** + * Long转 java.time.Instant + * + * @param time 时间戳 + * @return java.time.Instant + */ + private Object toInstant(Long time) { + return ClassUtil.invoke("java.time.Instant.ofEpochMilli", false, time); + } + + @SuppressWarnings("unchecked") + @Override + public Class getTargetType() { + return (Class) this.targetType; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/impl/LocaleConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/LocaleConverter.java new file mode 100644 index 000000000..f661042fb --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/convert/impl/MapConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/MapConverter.java new file mode 100644 index 000000000..bd8745786 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/convert/impl/MapConverter.java @@ -0,0 +1,97 @@ +package cn.hutool.core.convert.impl; + +import java.lang.reflect.Type; +import java.util.Map; +import java.util.Map.Entry; + +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; + +/** + * {@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 = null; + if (value instanceof Map) { + final Type[] typeArguments = TypeUtil.getTypeArguments(value.getClass()); + if (null != typeArguments // + && 2 == typeArguments.length// + && this.keyType.equals(typeArguments[0]) // + && this.valueType.equals(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); + } 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(); + Object key; + Object value; + for (Entry entry : srcMap.entrySet()) { + key = TypeUtil.isUnknow(this.keyType) ? entry.getKey() : convert.convert(this.keyType, entry.getKey()); + value = TypeUtil.isUnknow(this.valueType) ? entry.getValue() : convert.convert(this.valueType, entry.getValue()); + targetMap.put(key, value); + } + } + + @Override + @SuppressWarnings("unchecked") + public Class> getTargetType() { + return (Class>) TypeUtil.getClass(this.mapType); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/impl/NumberConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/NumberConverter.java new file mode 100644 index 000000000..2df1b6940 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/convert/impl/NumberConverter.java @@ -0,0 +1,209 @@ +package cn.hutool.core.convert.impl; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import cn.hutool.core.convert.AbstractConverter; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 数字转换器
+ * 支持类型为:
+ *
    + *
  • java.lang.Byte
  • + *
  • java.lang.Short
  • + *
  • java.lang.Integer
  • + *
  • java.lang.Long
  • + *
  • java.lang.Float
  • + *
  • java.lang.Double
  • + *
  • java.math.BigDecimal
  • + *
  • java.math.BigInteger
  • + *
+ * + * @author Looly + * + */ +public class NumberConverter extends AbstractConverter { + private static final long serialVersionUID = 1L; + + private Class targetType; + + public NumberConverter() { + this.targetType = Number.class; + } + + /** + * 构造
+ * + * @param clazz 需要转换的数字类型,默认 {@link Number} + */ + public NumberConverter(Class clazz) { + this.targetType = (null == clazz) ? Number.class : clazz; + } + + @Override + protected Number convertInternal(Object value) { + final Class targetType = this.targetType; + if (Byte.class == targetType) { + if (value instanceof Number) { + return Byte.valueOf(((Number) value).byteValue()); + } else if(value instanceof Boolean) { + return BooleanUtil.toByteObj((Boolean)value); + } + final String valueStr = convertToStr(value); + return StrUtil.isBlank(valueStr) ? null : Byte.valueOf(valueStr); + + } else if (Short.class == targetType) { + if (value instanceof Number) { + return Short.valueOf(((Number) value).shortValue()); + } else if(value instanceof Boolean) { + return BooleanUtil.toShortObj((Boolean)value); + } + final String valueStr = convertToStr(value); + return StrUtil.isBlank(valueStr) ? null : Short.valueOf(valueStr); + + } else if (Integer.class == targetType) { + if (value instanceof Number) { + return Integer.valueOf(((Number) value).intValue()); + } else if(value instanceof Boolean) { + return BooleanUtil.toInteger((Boolean)value); + } + final String valueStr = convertToStr(value); + return StrUtil.isBlank(valueStr) ? null : Integer.valueOf(NumberUtil.parseInt(valueStr)); + + } else if (AtomicInteger.class == targetType) { + int intValue; + if (value instanceof Number) { + intValue = ((Number) value).intValue(); + } else if(value instanceof Boolean) { + intValue = BooleanUtil.toInt((Boolean)value); + } + final String valueStr = convertToStr(value); + if (StrUtil.isBlank(valueStr)) { + return null; + } + intValue = NumberUtil.parseInt(valueStr); + return new AtomicInteger(intValue); + } else if (Long.class == targetType) { + if (value instanceof Number) { + return Long.valueOf(((Number) value).longValue()); + } else if(value instanceof Boolean) { + return BooleanUtil.toLongObj((Boolean)value); + } + final String valueStr = convertToStr(value); + return StrUtil.isBlank(valueStr) ? null : Long.valueOf(NumberUtil.parseLong(valueStr)); + + } else if (AtomicLong.class == targetType) { + long longValue; + if (value instanceof Number) { + longValue = ((Number) value).longValue(); + } else if(value instanceof Boolean) { + longValue = BooleanUtil.toLong((Boolean)value); + } + final String valueStr = convertToStr(value); + if (StrUtil.isBlank(valueStr)) { + return null; + } + longValue = NumberUtil.parseLong(valueStr); + return new AtomicLong(longValue); + + } else if (Float.class == targetType) { + if (value instanceof Number) { + return Float.valueOf(((Number) value).floatValue()); + } else if(value instanceof Boolean) { + return BooleanUtil.toFloatObj((Boolean)value); + } + final String valueStr = convertToStr(value); + return StrUtil.isBlank(valueStr) ? null : Float.valueOf(valueStr); + + } else if (Double.class == targetType) { + if (value instanceof Number) { + return Double.valueOf(((Number) value).doubleValue()); + } else if(value instanceof Boolean) { + return BooleanUtil.toDoubleObj((Boolean)value); + } + final String valueStr = convertToStr(value); + return StrUtil.isBlank(valueStr) ? null : Double.valueOf(valueStr); + + } else if (BigDecimal.class == targetType) { + return toBigDecimal(value); + + } else if (BigInteger.class == targetType) { + return toBigInteger(value); + + }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 = convertToStr(value); + return StrUtil.isBlank(valueStr) ? null : NumberUtil.parseNumber(valueStr); + } + + throw new UnsupportedOperationException(StrUtil.format("Unsupport Number type: {}", this.targetType.getName())); + } + + /** + * 转换为BigDecimal
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + private BigDecimal toBigDecimal(Object value) { + if (value instanceof Long) { + return new BigDecimal((Long) value); + } else if (value instanceof Integer) { + return new BigDecimal((Integer) value); + } else if (value instanceof BigInteger) { + return new BigDecimal((BigInteger) value); + } else if(value instanceof Boolean) { + return new BigDecimal((boolean)value ? 1 : 0); + } + + //对于Double类型,先要转换为String,避免精度问题 + final String valueStr = convertToStr(value); + if (StrUtil.isBlank(valueStr)) { + return null; + } + return new BigDecimal(valueStr); + } + + /** + * 转换为BigInteger
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + private BigInteger toBigInteger(Object value) { + if (value instanceof Long) { + return BigInteger.valueOf((Long) value); + } else if(value instanceof Boolean) { + return BigInteger.valueOf((boolean)value ? 1 : 0); + } + final String valueStr = convertToStr(value); + if (StrUtil.isBlank(valueStr)) { + return null; + } + return new BigInteger(valueStr); + } + + @Override + protected String convertToStr(Object value) { + return StrUtil.trim(super.convertToStr(value)); + } + + @Override + @SuppressWarnings("unchecked") + public Class getTargetType() { + return (Class) this.targetType; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/impl/PathConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/PathConverter.java new file mode 100644 index 000000000..fc7cc5672 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/convert/impl/PrimitiveConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/PrimitiveConverter.java new file mode 100644 index 000000000..13218ac0c --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/convert/impl/PrimitiveConverter.java @@ -0,0 +1,153 @@ +package cn.hutool.core.convert.impl; + +import cn.hutool.core.convert.AbstractConverter; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 原始类型转换器
+ * 支持类型为:
+ *
    + *
  • byte
  • + *
  • short
  • + *
  • int
  • + *
  • long
  • + *
  • float
  • + *
  • double
  • + *
  • char
  • + *
  • boolean
  • + *
+ * + * @author Looly + * + */ +public class PrimitiveConverter extends AbstractConverter { + private static final long serialVersionUID = 1L; + + private Class targetType; + + /** + * 构造
+ * @param clazz 需要转换的原始 + * @throws IllegalArgumentException 传入的转换类型非原始类型时抛出 + */ + public PrimitiveConverter(Class clazz) { + if(null == clazz){ + throw new NullPointerException("PrimitiveConverter not allow null target type!"); + }else if(false == clazz.isPrimitive()){ + throw new IllegalArgumentException("[" + clazz + "] is not a primitive class!"); + } + this.targetType = clazz; + } + + @Override + protected Object convertInternal(Object value) { + try { + if (byte.class == this.targetType) { + if (value instanceof Number) { + return ((Number) value).byteValue(); + } else if(value instanceof Boolean) { + return BooleanUtil.toByte((Boolean)value); + } + final String valueStr = convertToStr(value); + if (StrUtil.isBlank(valueStr)) { + return 0; + } + return Byte.parseByte(valueStr); + + } else if (short.class == this.targetType) { + if (value instanceof Number) { + return ((Number) value).shortValue(); + } else if(value instanceof Boolean) { + return BooleanUtil.toShort((Boolean)value); + } + final String valueStr = convertToStr(value); + if (StrUtil.isBlank(valueStr)) { + return 0; + } + return Short.parseShort(valueStr); + + } else if (int.class == this.targetType) { + if (value instanceof Number) { + return ((Number) value).intValue(); + } else if(value instanceof Boolean) { + return BooleanUtil.toInt((Boolean)value); + } + final String valueStr = convertToStr(value); + if (StrUtil.isBlank(valueStr)) { + return 0; + } + return NumberUtil.parseInt(valueStr); + + } else if (long.class == this.targetType) { + if (value instanceof Number) { + return ((Number) value).longValue(); + } else if(value instanceof Boolean) { + return BooleanUtil.toLong((Boolean)value); + } + final String valueStr = convertToStr(value); + if (StrUtil.isBlank(valueStr)) { + return 0; + } + return NumberUtil.parseLong(valueStr); + + } else if (float.class == this.targetType) { + if (value instanceof Number) { + return ((Number) value).floatValue(); + } else if(value instanceof Boolean) { + return BooleanUtil.toFloat((Boolean)value); + } + final String valueStr = convertToStr(value); + if (StrUtil.isBlank(valueStr)) { + return 0; + } + return Float.parseFloat(valueStr); + + } else if (double.class == this.targetType) { + if (value instanceof Number) { + return ((Number) value).doubleValue(); + } else if(value instanceof Boolean) { + return BooleanUtil.toDouble((Boolean)value); + } + final String valueStr = convertToStr(value); + if (StrUtil.isBlank(valueStr)) { + return 0; + } + return Double.parseDouble(valueStr); + + } else if (char.class == this.targetType) { + if(value instanceof Character){ + return ((Character)value).charValue(); + } else if(value instanceof Boolean) { + return BooleanUtil.toChar((Boolean)value); + } + final String valueStr = convertToStr(value); + if (StrUtil.isBlank(valueStr)) { + return 0; + } + return valueStr.charAt(0); + } else if (boolean.class == this.targetType) { + if(value instanceof Boolean){ + return ((Boolean)value).booleanValue(); + } + String valueStr = convertToStr(value); + return BooleanUtil.toBoolean(valueStr); + } + } catch (Exception e) { + // Ignore Exception + } + return 0; + } + + @Override + protected String convertToStr(Object value) { + return StrUtil.trim(super.convertToStr(value)); + } + + @Override + @SuppressWarnings("unchecked") + public Class getTargetType() { + return (Class) this.targetType; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/impl/ReferenceConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/ReferenceConverter.java new file mode 100644 index 000000000..8dd269a9f --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/convert/impl/ReferenceConverter.java @@ -0,0 +1,56 @@ +package cn.hutool.core.convert.impl; + +import java.lang.ref.Reference; +import java.lang.ref.SoftReference; +import java.lang.ref.WeakReference; +import java.lang.reflect.Type; + +import cn.hutool.core.convert.AbstractConverter; +import cn.hutool.core.convert.ConverterRegistry; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.TypeUtil; + +/** + * {@link Reference}转换器 + * + * @author Looly + * @since 3.0.8 + */ +@SuppressWarnings("rawtypes") +public class ReferenceConverter extends AbstractConverter { + private static final long serialVersionUID = 1L; + + private 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(false == TypeUtil.isUnknow(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/hutool-core/src/main/java/cn/hutool/core/convert/impl/StackTraceElementConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/StackTraceElementConverter.java new file mode 100644 index 000000000..6267ec727 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/convert/impl/StringConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/StringConverter.java new file mode 100644 index 000000000..f1a49e749 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/convert/impl/StringConverter.java @@ -0,0 +1,18 @@ +package cn.hutool.core.convert.impl; + +import cn.hutool.core.convert.AbstractConverter; + +/** + * 字符串转换器 + * @author Looly + * + */ +public class StringConverter extends AbstractConverter{ + private static final long serialVersionUID = 1L; + + @Override + protected String convertInternal(Object value) { + return convertToStr(value); + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/impl/TimeZoneConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/TimeZoneConverter.java new file mode 100644 index 000000000..9734b864a --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/convert/impl/URIConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/URIConverter.java new file mode 100644 index 000000000..2c6ef24b8 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/convert/impl/URLConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/URLConverter.java new file mode 100644 index 000000000..d28509218 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/convert/impl/UUIDConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/UUIDConverter.java new file mode 100644 index 000000000..129fc6595 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/convert/impl/package-info.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/package-info.java new file mode 100644 index 000000000..18f567b06 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/convert/package-info.java b/hutool-core/src/main/java/cn/hutool/core/convert/package-info.java new file mode 100644 index 000000000..28f8ee2c5 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/date/BetweenFormater.java b/hutool-core/src/main/java/cn/hutool/core/date/BetweenFormater.java new file mode 100644 index 000000000..99ad710fa --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/date/BetweenFormater.java @@ -0,0 +1,174 @@ +package cn.hutool.core.date; + +import java.io.Serializable; + +import cn.hutool.core.util.StrUtil; + +/** + * 时长格式化器 + * @author Looly + * + */ +public class BetweenFormater implements Serializable{ + private static final long serialVersionUID = 1L; + + /** 时长毫秒数 */ + private long betweenMs; + /** 格式化级别 */ + private Level level; + /** 格式化级别的最大个数 */ + private int levelMaxCount; + + /** + * 构造 + * @param betweenMs 日期间隔 + * @param level 级别,按照天、小时、分、秒、毫秒分为5个等级,根据传入等级,格式化到相应级别 + */ + public BetweenFormater(long betweenMs, Level level) { + this(betweenMs, level, 0); + } + + /** + * 构造 + * @param betweenMs 日期间隔 + * @param level 级别,按照天、小时、分、秒、毫秒分为5个等级,根据传入等级,格式化到相应级别 + * @param levelMaxCount 格式化级别的最大个数,假如级别个数为1,但是级别到秒,那只显示一个级别 + */ + public BetweenFormater(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; + long second = betweenMs / DateUnit.SECOND.getMillis() - ((day * 24 + hour) * 60 + minute) * 60; + long millisecond = betweenMs - (((day * 24 + hour) * 60 + minute) * 60 + 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.MILLSECOND.ordinal()){ + sb.append(millisecond).append(Level.MILLSECOND.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 static enum Level { + + /** 天 */ + DAY("天"), + /** 小时 */ + HOUR("小时"), + /** 分钟 */ + MINUTE("分"), + /** 秒 */ + SECOND("秒"), + /** 毫秒 */ + MILLSECOND("毫秒"); + + /** 级别名称 */ + private String name; + + /** + * 构造 + * @param name 级别名称 + */ + private 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/hutool-core/src/main/java/cn/hutool/core/date/DateBetween.java b/hutool-core/src/main/java/cn/hutool/core/date/DateBetween.java new file mode 100644 index 000000000..2a4f354a4 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/date/DateBetween.java @@ -0,0 +1,160 @@ +package cn.hutool.core.date; + +import java.io.Serializable; +import java.util.Calendar; +import java.util.Date; + +import cn.hutool.core.lang.Assert; + +/** + * 日期间隔 + * + * @author Looly + * + */ +public class DateBetween implements Serializable{ + private static final long serialVersionUID = 1L; + + /** 开始日期 */ + private Date begin; + /** 结束日期 */ + private Date end; + + /** + * 创建
+ * 在前的日期做为起始时间,在后的做为结束时间,间隔只保留绝对值正数 + * + * @param begin 起始时间 + * @param end 结束时间 + * @return {@link 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 {@link 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 (false == 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 (false == isReset) { + endCal.set(Calendar.YEAR, beginCal.get(Calendar.YEAR)); + long between = endCal.getTimeInMillis() - beginCal.getTimeInMillis(); + if (between < 0) { + return result - 1; + } + } + return result; + } + + /** + * 格式化输出时间差
+ * + * @param level 级别 + * @return 字符串 + */ + public String toString(BetweenFormater.Level level) { + return DateUtil.formatBetween(between(DateUnit.MS), level); + } + + @Override + public String toString() { + return toString(BetweenFormater.Level.MILLSECOND); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/date/DateException.java b/hutool-core/src/main/java/cn/hutool/core/date/DateException.java new file mode 100644 index 000000000..73abe73c3 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/date/DateField.java b/hutool-core/src/main/java/cn/hutool/core/date/DateField.java new file mode 100644 index 000000000..378faad39 --- /dev/null +++ b/hutool-core/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 int value; + + private DateField(int value) { + this.value = value; + } + + public int getValue() { + return this.value; + } + + /** + * 将 {@link Calendar}相关值转换为DatePart枚举对象
+ * + * @param calendarPartIntValue Calendar中关于Week的int值 + * @return {@link 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/hutool-core/src/main/java/cn/hutool/core/date/DateModifier.java b/hutool-core/src/main/java/cn/hutool/core/date/DateModifier.java new file mode 100644 index 000000000..5709b95f6 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/date/DateModifier.java @@ -0,0 +1,146 @@ +package cn.hutool.core.date; + +import java.util.Calendar; + +import cn.hutool.core.util.ArrayUtil; + +/** + * 日期修改器
+ * 用于实现自定义某个日期字段的调整,包括: + * + *
+ * 1. 获取指定字段的起始时间
+ * 2. 获取指定字段的四舍五入时间
+ * 3. 获取指定字段的结束时间
+ * 
+ * + * @author looly + * + */ +public class DateModifier { + + /** 忽略的字段 */ + private static final int[] ignoreFields = new int[] { // + Calendar.HOUR, // + Calendar.AM_PM, // + Calendar.DAY_OF_WEEK, // + Calendar.DAY_OF_YEAR, // + Calendar.WEEK_OF_YEAR// + }; + + /** + * 修改日期 + * + * @param calendar {@link Calendar} + * @param dateField 日期字段,既保留到哪个日期字段 + * @param modifyType 修改类型,包括舍去、四舍五入、进一等 + * @return 修改后的{@link Calendar} + */ + public static Calendar modify(Calendar calendar, int dateField, ModifyType modifyType) { + // 上下午特殊处理 + 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; + } + } + + // 当用户指定了无关字段时,降级字段 + if (ArrayUtil.contains(ignoreFields, dateField)) { + return modify(calendar, dateField + 1, modifyType); + } + + for (int i = Calendar.MILLISECOND; i > dateField; i--) { + if (ArrayUtil.contains(ignoreFields, i) || Calendar.WEEK_OF_MONTH == i) { + // 忽略无关字段(WEEK_OF_MONTH)始终不做修改 + continue; + } + + if (Calendar.WEEK_OF_MONTH == dateField) { + // 在星期模式下,月的处理忽略之 + if (Calendar.DAY_OF_MONTH == i) { + continue; + } else if (Calendar.DAY_OF_WEEK_IN_MONTH == i) { + // 星期模式下,星期几统一用DAY_OF_WEEK处理 + i = Calendar.DAY_OF_WEEK; + } + } else if (Calendar.DAY_OF_WEEK_IN_MONTH == i) { + // 非星期模式下,星期处理忽略之 + // 由于DAY_OF_WEEK忽略,自动降级到DAY_OF_WEEK_IN_MONTH + continue; + } + + modifyField(calendar, i, modifyType); + } + 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) { + // Console.log("# {} {}", DateField.of(field), calendar.getActualMinimum(field)); + 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; + } + } + // -------------------------------------------------------------------------------------------------- Private method end + + /** + * 修改类型 + * + * @author looly + * + */ + public static enum ModifyType { + /** + * 取指定日期短的起始值. + */ + TRUNCATE, + + /** + * 指定日期属性按照四舍五入处理 + */ + ROUND, + + /** + * 指定日期属性按照进一法处理 + */ + CEILING + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/date/DatePattern.java b/hutool-core/src/main/java/cn/hutool/core/date/DatePattern.java new file mode 100644 index 000000000..46e78f846 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/date/DatePattern.java @@ -0,0 +1,98 @@ +package cn.hutool.core.date; + +import java.util.Locale; +import java.util.TimeZone; + +import cn.hutool.core.date.format.FastDateFormat; + +/** + * 日期格式化类,提供常用的日期格式化对象 + * + * @author Looly + * + */ +public class DatePattern { + + //-------------------------------------------------------------------------------------------------------------------------------- Normal + /** 标准日期格式:yyyy-MM-dd */ + public final static String NORM_DATE_PATTERN = "yyyy-MM-dd"; + /** 标准日期格式 {@link FastDateFormat}:yyyy-MM-dd */ + public final static FastDateFormat NORM_DATE_FORMAT = FastDateFormat.getInstance(NORM_DATE_PATTERN); + + /** 标准时间格式:HH:mm:ss */ + public final static String NORM_TIME_PATTERN = "HH:mm:ss"; + /** 标准时间格式 {@link FastDateFormat}:HH:mm:ss */ + public final static FastDateFormat NORM_TIME_FORMAT = FastDateFormat.getInstance(NORM_TIME_PATTERN); + + /** 标准日期时间格式,精确到分:yyyy-MM-dd HH:mm */ + public final static String NORM_DATETIME_MINUTE_PATTERN = "yyyy-MM-dd HH:mm"; + /** 标准日期时间格式,精确到分 {@link FastDateFormat}:yyyy-MM-dd HH:mm */ + public final static FastDateFormat NORM_DATETIME_MINUTE_FORMAT = FastDateFormat.getInstance(NORM_DATETIME_MINUTE_PATTERN); + + /** 标准日期时间格式,精确到秒:yyyy-MM-dd HH:mm:ss */ + public final static String NORM_DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss"; + /** 标准日期时间格式,精确到秒 {@link FastDateFormat}:yyyy-MM-dd HH:mm:ss */ + public final static FastDateFormat NORM_DATETIME_FORMAT = FastDateFormat.getInstance(NORM_DATETIME_PATTERN); + + /** 标准日期时间格式,精确到毫秒:yyyy-MM-dd HH:mm:ss.SSS */ + public final static String NORM_DATETIME_MS_PATTERN = "yyyy-MM-dd HH:mm:ss.SSS"; + /** 标准日期时间格式,精确到毫秒 {@link FastDateFormat}:yyyy-MM-dd HH:mm:ss.SSS */ + public final static FastDateFormat NORM_DATETIME_MS_FORMAT = FastDateFormat.getInstance(NORM_DATETIME_MS_PATTERN); + + /** 标准日期格式:yyyy年MM月dd日 */ + public final static String CHINESE_DATE_PATTERN = "yyyy年MM月dd日"; + /** 标准日期格式 {@link FastDateFormat}:yyyy年MM月dd日 */ + public final static FastDateFormat CHINESE_DATE_FORMAT = FastDateFormat.getInstance(CHINESE_DATE_PATTERN); + + //-------------------------------------------------------------------------------------------------------------------------------- Pure + /** 标准日期格式:yyyyMMdd */ + public final static String PURE_DATE_PATTERN = "yyyyMMdd"; + /** 标准日期格式 {@link FastDateFormat}:yyyyMMdd */ + public final static FastDateFormat PURE_DATE_FORMAT = FastDateFormat.getInstance(PURE_DATE_PATTERN); + + /** 标准日期格式:HHmmss */ + public final static String PURE_TIME_PATTERN = "HHmmss"; + /** 标准日期格式 {@link FastDateFormat}:HHmmss */ + public final static FastDateFormat PURE_TIME_FORMAT = FastDateFormat.getInstance(PURE_TIME_PATTERN); + + /** 标准日期格式:yyyyMMddHHmmss */ + public final static String PURE_DATETIME_PATTERN = "yyyyMMddHHmmss"; + /** 标准日期格式 {@link FastDateFormat}:yyyyMMddHHmmss */ + public final static FastDateFormat PURE_DATETIME_FORMAT = FastDateFormat.getInstance(PURE_DATETIME_PATTERN); + + /** 标准日期格式:yyyyMMddHHmmssSSS */ + public final static String PURE_DATETIME_MS_PATTERN = "yyyyMMddHHmmssSSS"; + /** 标准日期格式 {@link FastDateFormat}:yyyyMMddHHmmssSSS */ + public final static FastDateFormat PURE_DATETIME_MS_FORMAT = FastDateFormat.getInstance(PURE_DATETIME_MS_PATTERN); + + //-------------------------------------------------------------------------------------------------------------------------------- Others + /** HTTP头中日期时间格式:EEE, dd MMM yyyy HH:mm:ss z */ + public final static String HTTP_DATETIME_PATTERN = "EEE, dd MMM yyyy HH:mm:ss z"; + /** HTTP头中日期时间格式 {@link FastDateFormat}:EEE, dd MMM yyyy HH:mm:ss z */ + public final static FastDateFormat HTTP_DATETIME_FORMAT = FastDateFormat.getInstance(HTTP_DATETIME_PATTERN, TimeZone.getTimeZone("GMT"), Locale.US); + + /** JDK中日期时间格式:EEE MMM dd HH:mm:ss zzz yyyy */ + public final static String JDK_DATETIME_PATTERN = "EEE MMM dd HH:mm:ss zzz yyyy"; + /** JDK中日期时间格式 {@link FastDateFormat}:EEE MMM dd HH:mm:ss zzz yyyy */ + public final static FastDateFormat JDK_DATETIME_FORMAT = FastDateFormat.getInstance(JDK_DATETIME_PATTERN, Locale.US); + + /** UTC时间:yyyy-MM-dd'T'HH:mm:ss'Z' */ + public final static String UTC_PATTERN = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + /** UTC时间{@link FastDateFormat}:yyyy-MM-dd'T'HH:mm:ss'Z' */ + public final static FastDateFormat UTC_FORMAT = FastDateFormat.getInstance(UTC_PATTERN, TimeZone.getTimeZone("UTC")); + + /** UTC时间:yyyy-MM-dd'T'HH:mm:ssZ */ + public final static String UTC_WITH_ZONE_OFFSET_PATTERN = "yyyy-MM-dd'T'HH:mm:ssZ"; + /** UTC时间{@link FastDateFormat}:yyyy-MM-dd'T'HH:mm:ssZ */ + public final static FastDateFormat UTC_WITH_ZONE_OFFSET_FORMAT = FastDateFormat.getInstance(UTC_WITH_ZONE_OFFSET_PATTERN, TimeZone.getTimeZone("UTC")); + + /** UTC时间:yyyy-MM-dd'T'HH:mm:ss.SSS'Z' */ + public final static 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 final static FastDateFormat UTC_MS_FORMAT = FastDateFormat.getInstance(UTC_MS_PATTERN, TimeZone.getTimeZone("UTC")); + + /** UTC时间:yyyy-MM-dd'T'HH:mm:ssZ */ + public final static 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 final static FastDateFormat UTC_MS_WITH_ZONE_OFFSET_FORMAT = FastDateFormat.getInstance(UTC_MS_WITH_ZONE_OFFSET_PATTERN, TimeZone.getTimeZone("UTC")); +} diff --git a/hutool-core/src/main/java/cn/hutool/core/date/DateRange.java b/hutool-core/src/main/java/cn/hutool/core/date/DateRange.java new file mode 100644 index 000000000..e756f3516 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/date/DateRange.java @@ -0,0 +1,63 @@ +package cn.hutool.core.date; + +import java.util.Date; + +import cn.hutool.core.lang.Range; + +/** + * 日期范围 + * + * @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, final DateField unit) { + this(start, end, unit, 1); + } + + /** + * 构造,包含开始和结束日期时间 + * + * @param start 起始日期时间 + * @param end 结束日期时间 + * @param unit 步进单位 + * @param step 步进数 + */ + public DateRange(Date start, Date end, final DateField unit, final 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, final DateField unit, final int step, boolean isIncludeStart, boolean isIncludeEnd) { + super(DateUtil.date(start), DateUtil.date(end), new Steper() { + + @Override + public DateTime step(DateTime current, DateTime end, int index) { + DateTime dt = current.offsetNew(unit, step); + if (dt.isAfter(end)) { + return null; + } + return current.offsetNew(unit, step); + } + }, isIncludeStart, isIncludeEnd); + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/date/DateTime.java b/hutool-core/src/main/java/cn/hutool/core/date/DateTime.java new file mode 100644 index 000000000..65292e372 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/date/DateTime.java @@ -0,0 +1,916 @@ +package cn.hutool.core.date; + +import java.sql.Timestamp; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +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.lang.Assert; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 包装java.util.Date + * + * @author xiaoleilu + * + */ +public class DateTime extends Date { + private static final long serialVersionUID = -5395712593979185936L; + + /** 是否可变对象 */ + private boolean mutable = true; + /** 一周的第一天,默认是周一, 在设置或获得 WEEK_OF_MONTH 或 WEEK_OF_YEAR 字段时,Calendar 必须确定一个月或一年的第一个星期,以此作为参考点。 */ + private Week firstDayOfWeek = Week.MONDAY; + /** 时区 */ + private TimeZone timeZone; + + /** + * 转换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); + } + + /** + * 构造 + * + * @see DatePattern + * @param dateStr Date字符串 + * @param format 格式 + * @return {@link DateTime} + */ + 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.getTime(), TimeZone.getDefault()); + } + + /** + * 给定日期的构造 + * + * @param date 日期 + * @param timeZone 时区 + * @since 4.1.2 + */ + public DateTime(Date date, TimeZone timeZone) { + this(date.getTime(), timeZone); + } + + /** + * 给定日期的构造 + * + * @param calendar {@link Calendar} + */ + public DateTime(Calendar calendar) { + this(calendar.getTime(), (TimeZone) null); + } + + /** + * 给定日期毫秒数的构造 + * + * @param timeMillis 日期毫秒数 + * @since 4.1.2 + */ + public DateTime(long timeMillis) { + this(timeMillis, (TimeZone) null); + } + + /** + * 给定日期毫秒数的构造 + * + * @param timeMillis 日期毫秒数 + * @param timeZone 时区 + * @since 4.1.2 + */ + public DateTime(long timeMillis, TimeZone timeZone) { + super(timeMillis); + if (null != timeZone) { + this.timeZone = timeZone; + } + } + + /** + * 构造 + * + * @see DatePattern + * @param dateStr Date字符串 + * @param format 格式 + */ + public DateTime(String dateStr, String format) { + this(dateStr, new SimpleDateFormat(format)); + } + + /** + * 构造 + * + * @see DatePattern + * @param dateStr Date字符串 + * @param dateFormat 格式化器 {@link SimpleDateFormat} + */ + public DateTime(String dateStr, DateFormat dateFormat) { + this(parse(dateStr, dateFormat), dateFormat.getTimeZone()); + } + + /** + * 构造 + * + * @see DatePattern + * @param dateStr Date字符串 + * @param dateParser 格式化器 {@link DateParser},可以使用 {@link FastDateFormat} + */ + public DateTime(String dateStr, DateParser dateParser) { + this(parse(dateStr, dateParser), dateParser.getTimeZone()); + } + + // -------------------------------------------------------------------- Constructor end + + // -------------------------------------------------------------------- offset start + /** + * 调整日期和时间
+ * 如果此对象为可变对象,返回自身,否则返回新对象,设置是否可变对象见{@link #setMutable(boolean)} + * + * @param datePart 调整的部分 {@link DateField} + * @param offset 偏移量,正数为向后偏移,负数为向前偏移 + * @return 如果此对象为可变对象,返回自身,否则返回新对象 + */ + public DateTime offset(DateField datePart, int offset) { + final Calendar cal = toCalendar(); + cal.add(datePart.getValue(), offset); + + DateTime dt = mutable ? this : ObjectUtil.clone(this); + return dt.setTimeInternal(cal.getTimeInMillis()); + } + + /** + * 调整日期和时间
+ * 返回调整后的新{@link DateTime},不影响原对象 + * + * @param datePart 调整的部分 {@link DateField} + * @param offset 偏移量,正数为向后偏移,负数为向前偏移 + * @return 如果此对象为可变对象,返回自身,否则返回新对象 + * @since 3.0.9 + */ + public DateTime offsetNew(DateField datePart, int offset) { + final Calendar cal = toCalendar(); + cal.add(datePart.getValue(), offset); + + DateTime dt = ObjectUtil.clone(this); + return dt.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 {@link DateTime} + */ + public DateTime setField(DateField field, int value) { + return setField(field.getValue(), value); + } + + /** + * 设置日期的某个部分
+ * 如果此对象为可变对象,返回自身,否则返回新对象,设置是否可变对象见{@link #setMutable(boolean)} + * + * @param field 表示日期的哪个部分的int值 {@link Calendar} + * @param value 值 + * @return {@link DateTime} + */ + public DateTime setField(int field, int value) { + final Calendar calendar = toCalendar(); + calendar.set(field, value); + + DateTime dt = this; + if (false == 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:第一季度
+ * 2:第二季度
+ * 3:第三季度
+ * 4:第四季度
+ * + * @return 第几个季度 + * @deprecated 请使用{@link Quarter}代替 + */ + @Deprecated + public int season() { + return monthStartFromOne() / 4 + 1; + } + + /** + * 获得当前日期所属季度
+ * + * @return 第几个季度 {@link Season} + * @deprecated 请使用{@link #quarterEnum}代替 + */ + @Deprecated + public Season seasonEnum() { + return Season.of(season()); + } + + /** + * 获得当前日期所属季度,从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开始计数
+ * 由于{@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); + } + + /** + * 获得指定日期是这个日期所在月份的第几天
+ * + * @return 天 + */ + public int dayOfMonth() { + return getField(DateField.DAY_OF_MONTH); + } + + /** + * 获得指定日期是星期几,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 millsecond() { + 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 + + /** + * 是否闰年 + * + * @see DateUtil#isLeapYear(int) + * @return 是否闰年 + */ + 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); + cal.setFirstDayOfWeek(firstDayOfWeek.getValue()); + 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 Timestamp} + * + * @return {@link Timestamp} + */ + public Timestamp toTimestamp() { + return new Timestamp(this.getTime()); + } + + /** + * 转为 {@link java.sql.Date} + * + * @return {@link java.sql.Date} + */ + public java.sql.Date toSqlDate() { + return new java.sql.Date(getTime()); + } + + /** + * 计算相差时长 + * + * @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, BetweenFormater.Level formatLevel) { + return new DateBetween(this, date).toString(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; + } + + /** + * 设置时区 + * + * @param timeZone 时区 + * @return this + * @since 4.1.2 + */ + public DateTime setTimeZone(TimeZone timeZone) { + this.timeZone = timeZone; + return this; + } + + // -------------------------------------------------------------------- toString start + /** + * 转为"yyyy-MM-dd yyyy-MM-dd HH:mm:ss " 格式字符串
+ * 如果时区被设置,会转换为其时区对应的时间,否则转换为当前地点对应的时区 + * + * @return "yyyy-MM-dd yyyy-MM-dd HH:mm:ss " 格式字符串 + */ + @Override + public String toString() { + return toString(this.timeZone); + } + + /** + * 转为"yyyy-MM-dd yyyy-MM-dd HH:mm:ss " 格式字符串
+ * 时区使用当前地区的默认时区 + * + * @return "yyyy-MM-dd yyyy-MM-dd HH:mm:ss " 格式字符串 + * @since 4.1.14 + */ + public String toStringDefaultTimeZone() { + return toString(TimeZone.getDefault()); + } + + /** + * 转为"yyyy-MM-dd yyyy-MM-dd HH:mm:ss " 格式字符串
+ * 如果时区不为{@code null},会转换为其时区对应的时间,否则转换为当前时间对应的时区 + * + * @param timeZone 时区 + * @return "yyyy-MM-dd yyyy-MM-dd HH:mm:ss " 格式字符串 + * @since 4.1.14 + */ + public String toString(TimeZone timeZone) { + if (null != timeZone) { + final SimpleDateFormat simpleDateFormat = new SimpleDateFormat(DatePattern.NORM_DATETIME_PATTERN); + simpleDateFormat.setTimeZone(timeZone); + return toString(simpleDateFormat); + } + return toString(DatePattern.NORM_DATETIME_FORMAT); + } + + /** + * 转为"yyyy-MM-dd " 格式字符串 + * + * @return "yyyy-MM-dd " 格式字符串 + * @since 4.0.0 + */ + public String toDateStr() { + if (null != this.timeZone) { + final SimpleDateFormat simpleDateFormat = new SimpleDateFormat(DatePattern.NORM_DATE_PATTERN); + simpleDateFormat.setTimeZone(this.timeZone); + return toString(simpleDateFormat); + } + return toString(DatePattern.NORM_DATE_FORMAT); + } + + /** + * 转为"HH:mm:ss" 格式字符串 + * + * @return "HH:mm:ss" 格式字符串 + * @since 4.1.4 + */ + public String toTimeStr() { + if (null != this.timeZone) { + final SimpleDateFormat simpleDateFormat = new SimpleDateFormat(DatePattern.NORM_TIME_PATTERN); + simpleDateFormat.setTimeZone(this.timeZone); + return toString(simpleDateFormat); + } + return toString(DatePattern.NORM_TIME_FORMAT); + } + + /** + * 转为字符串 + * + * @param format 日期格式,常用格式见: {@link DatePattern} + * @return String + */ + public String toString(String format) { + if (null != this.timeZone) { + final SimpleDateFormat simpleDateFormat = new SimpleDateFormat(format); + simpleDateFormat.setTimeZone(this.timeZone); + return toString(simpleDateFormat); + } + 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(String dateStr, DateFormat dateFormat) { + try { + return dateFormat.parse(dateStr); + } 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} + * @return {@link Date} + */ + private static Date parse(String dateStr, DateParser parser) { + Assert.notNull(parser, "Parser or DateFromat must be not null !"); + Assert.notBlank(dateStr, "Date String must be not blank !"); + try { + return parser.parse(dateStr); + } catch (Exception e) { + throw new DateException("Parse [{}] with format [{}] error!", dateStr, parser.getPattern(), e); + } + } + + /** + * 设置日期时间 + * + * @param time 日期时间毫秒 + * @return this + */ + private DateTime setTimeInternal(long time) { + super.setTime(time); + return this; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/date/DateUnit.java b/hutool-core/src/main/java/cn/hutool/core/date/DateUnit.java new file mode 100644 index 000000000..13a36c71d --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/date/DateUnit.java @@ -0,0 +1,33 @@ +package cn.hutool.core.date; + +/** + * 日期时间单位,每个单位都是以毫秒为基数 + * @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 long millis; + DateUnit(long millis){ + this.millis = millis; + } + + /** + * @return 单位对应的毫秒数 + */ + public long getMillis(){ + return this.millis; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java b/hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java new file mode 100644 index 000000000..ee836071e --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java @@ -0,0 +1,1923 @@ +package cn.hutool.core.date; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.comparator.CompareUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.date.DateModifier.ModifyType; +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.lang.PatternPool; +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.ReUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 时间工具类 + * + * @author xiaoleilu + */ +public class DateUtil { + + /** + * 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时间戳) + * @return 时间对象 + * @since 3.0.7 + */ + public static DateTime date(Date date) { + if (date instanceof DateTime) { + return (DateTime) date; + } + return dateNew(date); + } + + /** + * 根据已有{@link Date} 产生新的{@link DateTime}对象 + * + * @param date Date对象 + * @return {@link DateTime}对象 + * @since 4.3.1 + */ + public static DateTime dateNew(Date date) { + 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} + * @return 时间对象 + */ + public static DateTime date(Calendar calendar) { + return new DateTime(calendar); + } + + /** + * 转换为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) { + final Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(millis); + return cal; + } + + /** + * 当前时间的时间戳 + * + * @param isNano 是否为高精度时间 + * @return 时间 + */ + public static long current(boolean isNano) { + return isNano ? System.nanoTime() : 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(); + } + + /** + * 获得指定日期是所在年份的第几周
+ * + * @param date 日期 + * @return 周 + */ + 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(); + } + + /** + * 获得指定日期是星期几,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 日期 + * @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 millsecond(Date date) { + return DateTime.of(date).millsecond(); + } + + /** + * 是否为上午 + * + * @param date 日期 + * @return 是否为上午 + */ + public static boolean isAM(Date date) { + return DateTime.of(date).isAM(); + } + + /** + * 是否为上午 + * + * @param calendar {@link Calendar} + * @return 是否为上午 + * @since 4.5.7 + */ + public static boolean isAM(Calendar calendar) { + return Calendar.AM == calendar.get(Calendar.AM_PM); + } + + /** + * 是否为下午 + * + * @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 thisMillsecond() { + return millsecond(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()); + } + + /** + * 获得指定日期区间内的年份和季节
+ * + * @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; + } + + // ------------------------------------ Format start ---------------------------------------------- + /** + * 根据特定格式格式化日期 + * + * @param date 被格式化的日期 + * @param format 日期格式,常用格式见: {@link DatePattern} + * @return 格式化后的字符串 + */ + public static String format(Date date, String format) { + if (null == date || StrUtil.isBlank(format)) { + return null; + } + return format(date, FastDateFormat.getInstance(format)); + } + + /** + * 根据特定格式格式化日期 + * + * @param date 被格式化的日期 + * @param format {@link DatePrinter} 或 {@link FastDateFormat} + * @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); + } + + /** + * 格式化日期时间
+ * 格式 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 是否采用大写形式 + * @return 中文日期字符串 + * @since 4.1.19 + */ + public static String formatChineseDate(Date date, boolean isUppercase) { + if (null == date) { + return null; + } + + String format = DatePattern.CHINESE_DATE_FORMAT.format(date); + if (isUppercase) { + final StringBuilder builder = StrUtil.builder(format.length()); + builder.append(Convert.numberToChinese(Integer.parseInt(format.substring(0, 1)), false)); + builder.append(Convert.numberToChinese(Integer.parseInt(format.substring(1, 2)), false)); + builder.append(Convert.numberToChinese(Integer.parseInt(format.substring(2, 3)), false)); + builder.append(Convert.numberToChinese(Integer.parseInt(format.substring(3, 4)), false)); + builder.append(format.substring(4, 5)); + builder.append(Convert.numberToChinese(Integer.parseInt(format.substring(5, 7)), false)); + builder.append(format.substring(7, 8)); + builder.append(Convert.numberToChinese(Integer.parseInt(format.substring(8, 10)), false)); + builder.append(format.substring(10)); + format = builder.toString().replace('零', '〇'); + } + return format; + } + // ------------------------------------ Format end ---------------------------------------------- + + // ------------------------------------ Parse start ---------------------------------------------- + + /** + * 构建DateTime对象 + * + * @param dateStr Date字符串 + * @param dateFormat 格式化器 {@link SimpleDateFormat} + * @return DateTime对象 + */ + public static DateTime parse(String dateStr, DateFormat dateFormat) { + return new DateTime(dateStr, dateFormat); + } + + /** + * 构建DateTime对象 + * + * @param dateStr Date字符串 + * @param parser 格式化器,{@link FastDateFormat} + * @return DateTime对象 + */ + public static DateTime parse(String dateStr, DateParser parser) { + return new DateTime(dateStr, parser); + } + + /** + * 将特定格式的日期转换为Date对象 + * + * @param dateStr 特定格式的日期 + * @param format 格式,例如yyyy-MM-dd + * @return 日期对象 + */ + public static DateTime parse(String 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(String dateStr, String format, Locale locale) { + return new DateTime(dateStr, new SimpleDateFormat(format, locale)); + } + + /** + * 格式yyyy-MM-dd HH:mm:ss + * + * @param dateString 标准形式的时间字符串 + * @return 日期对象 + */ + public static DateTime parseDateTime(String dateString) { + dateString = normalize(dateString); + return parse(dateString, DatePattern.NORM_DATETIME_FORMAT); + } + + /** + * 解析格式为yyyy-MM-dd的日期,忽略时分秒 + * + * @param dateString 标准形式的日期字符串 + * @return 日期对象 + */ + public static DateTime parseDate(String dateString) { + dateString = normalize(dateString); + return parse(dateString, DatePattern.NORM_DATE_FORMAT); + } + + /** + * 解析时间,格式HH:mm:ss,日期部分默认为1970-01-01 + * + * @param timeString 标准形式的日期字符串 + * @return 日期对象 + */ + public static DateTime parseTime(String 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(String 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. + *
+ * + * @param utcString UTC时间 + * @return 日期对象 + * @since 4.1.14 + */ + public static DateTime parseUTC(String utcString) { + if (utcString == null) { + return null; + } + int length = utcString.length(); + if (StrUtil.contains(utcString, 'Z')) { + if (length == DatePattern.UTC_PATTERN.length() - 4) { + // 格式类似:2018-09-13T05:34:31Z + return parse(utcString, DatePattern.UTC_FORMAT); + } else if (length == DatePattern.UTC_MS_PATTERN.length() - 4) { + // 格式类似:2018-09-13T05:34:31.999Z + return parse(utcString, DatePattern.UTC_MS_FORMAT); + } + } else { + if (length == DatePattern.UTC_WITH_ZONE_OFFSET_PATTERN.length() + 2 || length == DatePattern.UTC_WITH_ZONE_OFFSET_PATTERN.length() + 3) { + // 格式类似:2018-09-13T05:34:31+0800 或 2018-09-13T05:34:31+08:00 + return parse(utcString, DatePattern.UTC_WITH_ZONE_OFFSET_FORMAT); + } else if (length == DatePattern.UTC_MS_WITH_ZONE_OFFSET_PATTERN.length() + 2 || length == DatePattern.UTC_MS_WITH_ZONE_OFFSET_PATTERN.length() + 3) { + // 格式类似:2018-09-13T05:34:31.999+0800 或 2018-09-13T05:34:31.999+08:00 + return parse(utcString, DatePattern.UTC_MS_WITH_ZONE_OFFSET_FORMAT); + } + } + // 没有更多匹配的时间格式 + throw new DateException("No format fit for date String [{}] !", utcString); + } + + /** + * 将日期字符串转换为{@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. 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 日期字符串 + * @return 日期 + */ + public static DateTime parse(String dateStr) { + if (null == dateStr) { + return null; + } + // 去掉两边空格并去掉中文日期中的“日”和“秒”,以规范长度 + dateStr = StrUtil.removeAll(dateStr.trim(), '日', '秒'); + int length = dateStr.length(); + + if (Validator.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 + return parse(dateStr, DatePattern.JDK_DATETIME_FORMAT); + } else if (StrUtil.contains(dateStr, 'T')) { + // UTC时间 + return parseUTC(dateStr); + } + + if (length == DatePattern.NORM_DATETIME_PATTERN.length()) { + // yyyy-MM-dd HH:mm:ss + return parseDateTime(dateStr); + } else if (length == DatePattern.NORM_DATE_PATTERN.length()) { + // yyyy-MM-dd + return parseDate(dateStr); + } else if (length == DatePattern.NORM_DATETIME_MINUTE_PATTERN.length()) { + // yyyy-MM-dd HH:mm + return parse(normalize(dateStr), DatePattern.NORM_DATETIME_MINUTE_FORMAT); + } else if (length >= DatePattern.NORM_DATETIME_MS_PATTERN.length() - 2) { + return parse(normalize(dateStr), DatePattern.NORM_DATETIME_MS_FORMAT); + } + + // 没有更多匹配的时间格式 + throw new DateException("No format fit for date String [{}] !", dateStr); + } + + // ------------------------------------ Parse end ---------------------------------------------- + + // ------------------------------------ Offset start ---------------------------------------------- + /** + * 修改日期为某个时间字段起始时间 + * + * @param date {@link Date} + * @param dateField 时间字段 + * @return {@link DateTime} + * @since 4.5.7 + */ + public static DateTime truncate(Date date, DateField dateField) { + return new DateTime(truncate(calendar(date), dateField)); + } + + /** + * 修改日期为某个时间字段起始时间 + * + * @param calendar {@link Calendar} + * @param dateField 时间字段 + * @return 原{@link Calendar} + * @since 4.5.7 + */ + public static Calendar truncate(Calendar calendar, DateField dateField) { + return DateModifier.modify(calendar, dateField.getValue(), ModifyType.TRUNCATE); + } + + /** + * 修改日期为某个时间字段四舍五入时间 + * + * @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 calendar {@link Calendar} + * @param dateField 时间字段 + * @return 原{@link Calendar} + * @since 4.5.7 + */ + public static Calendar round(Calendar calendar, DateField dateField) { + return DateModifier.modify(calendar, dateField.getValue(), ModifyType.ROUND); + } + + /** + * 修改日期为某个时间字段结束时间 + * + * @param date {@link Date} + * @param dateField 时间字段 + * @return {@link DateTime} + * @since 4.5.7 + */ + public static DateTime ceiling(Date date, DateField dateField) { + return new DateTime(ceiling(calendar(date), dateField)); + } + + /** + * 修改日期为某个时间字段结束时间 + * + * @param calendar {@link Calendar} + * @param dateField 时间字段 + * @return 原{@link Calendar} + * @since 4.5.7 + */ + public static Calendar ceiling(Calendar calendar, DateField dateField) { + return DateModifier.modify(calendar, dateField.getValue(), ModifyType.CEILING); + } + + /** + * 获取秒级别的开始时间,既忽略毫秒部分 + * + * @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 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 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 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 date 日期 + * @return {@link DateTime} + */ + public static DateTime beginOfWeek(Date date) { + return new DateTime(beginOfWeek(calendar(date))); + } + + /** + * 获取某周的结束时间 + * + * @param date 日期 + * @return {@link DateTime} + */ + public static DateTime endOfWeek(Date date) { + return new DateTime(endOfWeek(calendar(date))); + } + + /** + * 获取某周的开始时间 + * + * @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) { + if (isMondayAsFirstDay) { + calendar.setFirstDayOfWeek(Calendar.MONDAY); + } + 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} + * @since 3.1.2 + */ + public static Calendar endOfWeek(Calendar calendar, boolean isSundayAsLastDay) { + if (isSundayAsLastDay) { + calendar.setFirstDayOfWeek(Calendar.MONDAY); + } + return ceiling(calendar, DateField.WEEK_OF_MONTH); + } + + /** + * 获取某月的开始时间 + * + * @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 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 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 calendar 日期 {@link Calendar} + * @return {@link Calendar} + * @since 4.1.0 + */ + public static Calendar beginOfQuarter(Calendar calendar) { + 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 + */ + public static Calendar endOfQuarter(Calendar calendar) { + calendar.set(Calendar.MONTH, calendar.get(DateField.MONTH.getValue()) / 3 * 3 + 2); + calendar.set(Calendar.DAY_OF_MONTH, calendar.getActualMaximum(Calendar.DAY_OF_MONTH)); + return endOfDay(calendar); + } + + /** + * 获取某年的开始时间 + * + * @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))); + } + + /** + * 获取某年的开始时间 + * + * @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); + } + + // --------------------------------------------------- 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); + } + + /** + * 偏移天 + * + * @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) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + cal.add(dateField.getValue(), offset); + return new DateTime(cal.getTime()); + } + + /** + * 获取指定日期偏移指定时间后的时间 + * + * @param date 基准日期 + * @param dateField 偏移的粒度大小(小时、天、月等){@link DateField} + * @param offset 偏移量,正数为向后偏移,负数为向前偏移 + * @return 偏移后的日期 + * @deprecated please use {@link DateUtil#offset(Date, DateField, int)} + */ + @Deprecated + public static DateTime offsetDate(Date date, DateField dateField, int offset) { + return offset(date, 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为false相差天数为0。
+	 * 如果isReset为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); + } + + /** + * 计算两个日期相差月数
+ * 在非重置情况下,如果起始日期的天小于结束日期的天,月数要少算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, BetweenFormater.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, BetweenFormater.Level level) { + return new BetweenFormater(betweenMs, level).format(); + } + + /** + * 格式化日期间隔输出,精确到毫秒 + * + * @param betweenMs 日期间隔 + * @return XX天XX小时XX分XX秒XX毫秒 + * @since 3.0.1 + */ + public static String formatBetween(long betweenMs) { + return new BetweenFormater(betweenMs, BetweenFormater.Level.MILLSECOND).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 isSameDay(calendar(date1), calendar(date2)); + } + + /** + * 比较两个日期是否为同一天 + * + * @param cal1 日期1 + * @param cal2 日期2 + * @return 是否为同一天 + * @since 4.1.13 + */ + 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 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 + */ + public static int toIntSecond(Date date) { + return Integer.parseInt(DateUtil.format(date, "yyMMddHHmm")); + } + + /** + * 计算指定指定时间区间内的周数 + * + * @param start 开始时间 + * @param end 结束时间 + * @return 周数 + */ + public static int weekCount(Date start, Date end) { + final Calendar startCalendar = Calendar.getInstance(); + startCalendar.setTime(start); + final Calendar endCalendar = Calendar.getInstance(); + endCalendar.setTime(end); + + final int startWeekofYear = startCalendar.get(Calendar.WEEK_OF_YEAR); + final int endWeekofYear = endCalendar.get(Calendar.WEEK_OF_YEAR); + + int count = endWeekofYear - startWeekofYear + 1; + + if (Calendar.SUNDAY != startCalendar.get(Calendar.DAY_OF_WEEK)) { + count--; + } + + return count; + } + + /** + * 计时器
+ * 计算某个过程花费的时间,精确到毫秒 + * + * @return Timer + */ + public static TimeInterval timer() { + return new TimeInterval(); + + } + + /** + * 生日转为年龄,计算法定年龄 + * + * @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()); + } + + /** + * 计算相对于dateToCompare的年龄,长用于计算指定生日在某年的年龄 + * + * @param birthDay 生日 + * @param dateToCompare 需要对比的日期 + * @return 年龄 + */ + public static int age(Date birthDay, Date dateToCompare) { + Calendar cal = Calendar.getInstance(); + cal.setTime(dateToCompare); + + if (cal.before(birthDay)) { + throw new IllegalArgumentException(StrUtil.format("Birthday is after date {}!", formatDate(dateToCompare))); + } + + int year = cal.get(Calendar.YEAR); + int month = cal.get(Calendar.MONTH); + int dayOfMonth = cal.get(Calendar.DAY_OF_MONTH); + + cal.setTime(birthDay); + int age = year - cal.get(Calendar.YEAR); + + int monthBirth = cal.get(Calendar.MONTH); + if (month == monthBirth) { + int dayOfMonthBirth = cal.get(Calendar.DAY_OF_MONTH); + if (dayOfMonth < dayOfMonthBirth) { + // 如果生日在当月,但是未达到生日当天的日期,年龄减一 + age--; + } + } else if (month < monthBirth) { + // 如果当前月份未达到生日的月份,年龄计算减一 + age--; + } + + return age; + } + + /** + * 是否闰年 + * + * @param year 年 + * @return 是否闰年 + */ + public static boolean isLeapYear(int year) { + return new GregorianCalendar().isLeapYear(year); + } + + /** + * 判定给定开始时间经过某段时间后是否过期 + * + * @param startDate 开始时间 + * @param dateField 时间单位 + * @param timeLength 时长 + * @param checkedDate 被比较的时间。如果经过时长后的时间晚于被检查的时间,就表示过期 + * @return 是否过期 + * @since 3.1.1 + */ + public static boolean isExpired(Date startDate, DateField dateField, int timeLength, Date checkedDate) { + final Date endDate = offset(startDate, dateField, timeLength); + return endDate.after(checkedDate); + } + + /** + * 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 结束日期时间 + * @param unit 步进单位 + * @return {@link DateRange} + */ + public static List rangeToList(Date start, Date end, final DateField unit) { + return CollUtil.newArrayList((Iterable) range(start, end, unit)); + } + + /** + * 通过生日计算星座 + * + * @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); + } + + /** + * 获取指定日期字段的最小值,例如分钟的最小值是0 + * + * @param calendar {@link Calendar} + * @param dateField {@link DateField} + * @return 字段最小值 + * @since 4.5.7 + * @see Calendar#getActualMinimum(int) + */ + 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 字段最大值 + * @since 4.5.7 + * @see Calendar#getActualMaximum(int) + */ + public static int getEndValue(Calendar calendar, int dateField) { + if (Calendar.DAY_OF_WEEK == dateField) { + return (calendar.getFirstDayOfWeek() + 6) % 7; + } + return calendar.getActualMaximum(dateField); + } + + /** + * {@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}安全的{@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); + } + + // ------------------------------------------------------------------------ Private method start + /** + * 获得指定日期年份和季节
+ * 格式:[20131]表示2013年第一季度 + * + * @param cal 日期 + */ + private static String yearAndQuarter(Calendar cal) { + return new StringBuilder().append(cal.get(Calendar.YEAR)).append(cal.get(Calendar.MONTH) / 3 + 1).toString(); + } + + /** + * 标准化日期,默认处理以空格区分的日期时间格式,空格前为日期,空格后为时间:
+ * 将以下字符替换为"-" + * + *
+	 * "."
+	 * "/"
+	 * "年"
+	 * "月"
+	 * 
+ * + * 将以下字符去除 + * + *
+	 * "日"
+	 * 
+ * + * 将以下字符替换为":" + * + *
+	 * "时"
+	 * "分"
+	 * "秒"
+	 * 
+ * + * 当末位是":"时去除之(不存在毫秒时) + * + * @param dateStr 日期时间字符串 + * @return 格式化后的日期字符串 + */ + private static String normalize(String dateStr) { + if (StrUtil.isBlank(dateStr)) { + return dateStr; + } + + // 日期时间分开处理 + final List dateAndTime = StrUtil.splitTrim(dateStr, ' '); + final int size = dateAndTime.size(); + if (size < 1 || size > 2) { + // 非可被标准处理的格式 + return 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, ":"); + builder.append(timePart); + } + + return builder.toString(); + } + // ------------------------------------------------------------------------ Private method end +} diff --git a/hutool-core/src/main/java/cn/hutool/core/date/Month.java b/hutool-core/src/main/java/cn/hutool/core/date/Month.java new file mode 100644 index 000000000..1fcf8f406 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/date/Month.java @@ -0,0 +1,118 @@ +package cn.hutool.core.date; + +import java.util.Calendar; + +/** + * 月份枚举
+ * 与Calendar中的月份int值对应 + * + * @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 + * + * @author Looly + * + */ +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); + + // --------------------------------------------------------------- + private int value; + + private Month(int value) { + this.value = value; + } + + public int getValue() { + return this.value; + } + + /** + * 将 {@link Calendar}月份相关值转换为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 + * + * @param calendarMonthIntValue Calendar中关于Month的int值 + * @return {@link Month} + */ + public static Month of(int calendarMonthIntValue) { + switch (calendarMonthIntValue) { + case Calendar.JANUARY: + return JANUARY; + case Calendar.FEBRUARY: + return FEBRUARY; + case Calendar.MARCH: + return MARCH; + case Calendar.APRIL: + return APRIL; + case Calendar.MAY: + return MAY; + case Calendar.JUNE: + return JUNE; + case Calendar.JULY: + return JULY; + case Calendar.AUGUST: + return AUGUST; + case Calendar.SEPTEMBER: + return SEPTEMBER; + case Calendar.OCTOBER: + return OCTOBER; + case Calendar.NOVEMBER: + return NOVEMBER; + case Calendar.DECEMBER: + return DECEMBER; + case Calendar.UNDECIMBER: + return UNDECIMBER; + default: + return null; + } + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/date/Quarter.java b/hutool-core/src/main/java/cn/hutool/core/date/Quarter.java new file mode 100644 index 000000000..5d40bb3a5 --- /dev/null +++ b/hutool-core/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 int value; + + private 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/hutool-core/src/main/java/cn/hutool/core/date/Season.java b/hutool-core/src/main/java/cn/hutool/core/date/Season.java new file mode 100644 index 000000000..aeba4c3f5 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/date/Season.java @@ -0,0 +1,64 @@ +package cn.hutool.core.date; + +/** + * 季度枚举
+ * + * @see #SPRING + * @see #SUMMER + * @see #AUTUMN + * @see #WINTER + * + * @author Looly + *@deprecated 请使用{@link Quarter}代替 + */ +@Deprecated +public enum Season { + + /** 春季(第一季度) */ + SPRING(1), + /** 夏季(第二季度) */ + SUMMER(2), + /** 秋季(第三季度) */ + AUTUMN(3), + /** 冬季(第四季度) */ + WINTER(4); + + // --------------------------------------------------------------- + private int value; + + private Season(int value) { + this.value = value; + } + + public int getValue() { + return this.value; + } + + /** + * 将 季度int转换为Season枚举对象
+ * + * @see #SPRING + * @see #SUMMER + * @see #AUTUMN + * @see #WINTER + * + * @param intValue 季度int表示 + * @return {@link Season} + * @deprecated 使用@{@link Quarter} 替代 + */ + @Deprecated + public static Season of(int intValue) { + switch (intValue) { + case 1: + return SPRING; + case 2: + return SUMMER; + case 3: + return AUTUMN; + case 4: + return WINTER; + default: + return null; + } + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/date/SystemClock.java b/hutool-core/src/main/java/cn/hutool/core/date/SystemClock.java new file mode 100644 index 000000000..66f991afd --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/date/SystemClock.java @@ -0,0 +1,94 @@ +package cn.hutool.core.date; + +import java.sql.Timestamp; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +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 + */ + private SystemClock(long period) { + this.period = period; + this.now = System.currentTimeMillis(); + scheduleClockUpdating(); + } + + /** + * 开启计时器线程 + */ + private void scheduleClockUpdating() { + ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(new ThreadFactory(){ + @Override + public Thread newThread(Runnable runnable) { + Thread thread = new Thread(runnable, "System Clock"); + thread.setDaemon(true); + return thread; + } + }); + scheduler.scheduleAtFixedRate(new Runnable(){ + @Override + public void run() { + 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 单例实例 + */ + private static SystemClock instance() { + return InstanceHolder.INSTANCE; + } + + /** + * @return 当前时间 + */ + public static long now() { + return instance().currentTimeMillis(); + } + + /** + * @return 当前时间字符串表现形式 + */ + public static String nowDate() { + return new Timestamp(instance().currentTimeMillis()).toString(); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/date/TimeInterval.java b/hutool-core/src/main/java/cn/hutool/core/date/TimeInterval.java new file mode 100644 index 000000000..b8f8e0cb7 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/date/TimeInterval.java @@ -0,0 +1,113 @@ +package cn.hutool.core.date; + +import java.io.Serializable; + +/** + * 计时器
+ * 计算某个过程花费的时间,精确到毫秒 + * + * @author Looly + * + */ +public class TimeInterval implements Serializable{ + private static final long serialVersionUID = 1L; + + private long time; + private boolean isNano; + + public TimeInterval() { + this(false); + } + + public TimeInterval(boolean isNano) { + this.isNano = isNano; + start(); + } + + /** + * @return 开始计时并返回当前时间 + */ + public long start() { + time = DateUtil.current(isNano); + return time; + } + + /** + * @return 重新计时并返回从开始到当前的持续时间 + */ + public long intervalRestart() { + long now = DateUtil.current(isNano); + long d = now - time; + time = now; + return d; + } + + /** + * 重新开始计算时间(重置开始时间) + * @return this + * @since 3.0.1 + */ + public TimeInterval restart(){ + time = DateUtil.current(isNano); + return this; + } + + //----------------------------------------------------------- Interval + /** + * 从开始到当前的间隔时间(毫秒数)
+ * 如果使用纳秒计时,返回纳秒差,否则返回毫秒差 + * @return 从开始到当前的间隔时间(毫秒数) + */ + public long interval() { + return DateUtil.current(isNano) - time; + } + + /** + * 从开始到当前的间隔时间(毫秒数) + * @return 从开始到当前的间隔时间(毫秒数) + */ + public long intervalMs() { + return isNano ? interval() / 1000000L : interval(); + } + + /** + * 从开始到当前的间隔秒数,取绝对值 + * @return 从开始到当前的间隔秒数,取绝对值 + */ + public long intervalSecond(){ + return intervalMs() / DateUnit.SECOND.getMillis(); + } + + /** + * 从开始到当前的间隔分钟数,取绝对值 + * @return 从开始到当前的间隔分钟数,取绝对值 + */ + public long intervalMinute(){ + return intervalMs() / DateUnit.MINUTE.getMillis(); + } + + /** + * 从开始到当前的间隔小时数,取绝对值 + * @return 从开始到当前的间隔小时数,取绝对值 + */ + public long intervalHour(){ + return intervalMs() / DateUnit.HOUR.getMillis(); + } + + /** + * 从开始到当前的间隔天数,取绝对值 + * @return 从开始到当前的间隔天数,取绝对值 + */ + public long intervalDay(){ + return intervalMs() / DateUnit.DAY.getMillis(); + } + + /** + * 从开始到当前的间隔周数,取绝对值 + * @return 从开始到当前的间隔周数,取绝对值 + */ + public long intervalWeek(){ + return intervalMs() / DateUnit.WEEK.getMillis(); + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/date/Week.java b/hutool-core/src/main/java/cn/hutool/core/date/Week.java new file mode 100644 index 000000000..20d6c4732 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/date/Week.java @@ -0,0 +1,131 @@ +package cn.hutool.core.date; + +import java.util.Calendar; + +/** + * 星期枚举
+ * 与Calendar中的星期int值对应 + * + * @see #SUNDAY + * @see #MONDAY + * @see #TUESDAY + * @see #WEDNESDAY + * @see #THURSDAY + * @see #FRIDAY + * @see #SATURDAY + * + * @author Looly + * + */ +public enum Week { + + /** 周日 */ + SUNDAY(Calendar.SUNDAY), + /** 周一 */ + MONDAY(Calendar.MONDAY), + /** 周二 */ + TUESDAY(Calendar.TUESDAY), + /** 周三 */ + WEDNESDAY(Calendar.WEDNESDAY), + /** 周四 */ + THURSDAY(Calendar.THURSDAY), + /** 周五 */ + FRIDAY(Calendar.FRIDAY), + /** 周六 */ + SATURDAY(Calendar.SATURDAY); + + // --------------------------------------------------------------- + /** 星期对应{@link Calendar} 中的Week值 */ + private int value; + + /** + * 构造 + * + * @param value 星期对应{@link Calendar} 中的Week值 + */ + private Week(int value) { + this.value = value; + } + + /** + * 获得星期对应{@link Calendar} 中的Week值 + * + * @return 星期对应{@link Calendar} 中的Week值 + */ + public int getValue() { + return this.value; + } + + /** + * 转换为中文名 + * + * @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 Calendar}星期相关值转换为Week枚举对象
+ * + * @see #SUNDAY + * @see #MONDAY + * @see #TUESDAY + * @see #WEDNESDAY + * @see #THURSDAY + * @see #FRIDAY + * @see #SATURDAY + * + * @param calendarWeekIntValue Calendar中关于Week的int值 + * @return {@link Week} + */ + public static Week of(int calendarWeekIntValue) { + switch (calendarWeekIntValue) { + case Calendar.SUNDAY: + return SUNDAY; + case Calendar.MONDAY: + return MONDAY; + case Calendar.TUESDAY: + return TUESDAY; + case Calendar.WEDNESDAY: + return WEDNESDAY; + case Calendar.THURSDAY: + return THURSDAY; + case Calendar.FRIDAY: + return FRIDAY; + case Calendar.SATURDAY: + return SATURDAY; + default: + return null; + } + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/date/Zodiac.java b/hutool-core/src/main/java/cn/hutool/core/date/Zodiac.java new file mode 100644 index 000000000..27cdfb30d --- /dev/null +++ b/hutool-core/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[] dayArr = 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 < dayArr[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/hutool-core/src/main/java/cn/hutool/core/date/format/AbstractDateBasic.java b/hutool-core/src/main/java/cn/hutool/core/date/format/AbstractDateBasic.java new file mode 100644 index 000000000..ea1189cae --- /dev/null +++ b/hutool-core/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 == false) { + 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/hutool-core/src/main/java/cn/hutool/core/date/format/DateBasic.java b/hutool-core/src/main/java/cn/hutool/core/date/format/DateBasic.java new file mode 100644 index 000000000..397a78664 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/date/format/DateBasic.java @@ -0,0 +1,33 @@ +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/hutool-core/src/main/java/cn/hutool/core/date/format/DateParser.java b/hutool-core/src/main/java/cn/hutool/core/date/format/DateParser.java new file mode 100644 index 000000000..27caf815f --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/date/format/DateParser.java @@ -0,0 +1,68 @@ +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); + + /** + * 根据给定格式转换日期字符串 + * Updates the Calendar with parsed fields. 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 String whose beginning should be parsed. + * @return a java.util.Date object + * @throws ParseException if the beginning of the specified string cannot be parsed. + * @see java.text.DateFormat#parseObject(String) + */ + Object parseObject(String source) throws ParseException; + + /** + * 根据 {@link ParsePosition} 给定将日期字符串解析并转换为 {@link Date} 对象
+ * + * @param source A String whose beginning should be parsed. + * @param pos the parse position + * @return a java.util.Date object + * @see java.text.DateFormat#parseObject(String, ParsePosition) + */ + Object parseObject(String source, ParsePosition pos); +} diff --git a/hutool-core/src/main/java/cn/hutool/core/date/format/DatePrinter.java b/hutool-core/src/main/java/cn/hutool/core/date/format/DatePrinter.java new file mode 100644 index 000000000..92df7c543 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/date/format/FastDateFormat.java b/hutool-core/src/main/java/cn/hutool/core/date/format/FastDateFormat.java new file mode 100644 index 000000000..4a312249c --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/date/format/FastDateFormat.java @@ -0,0 +1,396 @@ +package cn.hutool.core.date.format; + +import java.text.DateFormat; +import java.text.FieldPosition; +import java.text.Format; +import java.text.ParseException; +import java.text.ParsePosition; +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; + + // ----------------------------------------------------------------------- + /** + * 获得 {@link FastDateFormat} 实例,使用默认格式和地区 + * + * @return {@link FastDateFormat} + */ + public static FastDateFormat getInstance() { + return cache.getInstance(); + } + + /** + * 获得 {@link FastDateFormat} 实例,使用默认地区
+ * 支持缓存 + * + * @param pattern 使用{@link java.text.SimpleDateFormat} 相同的日期格式 + * @return {@link FastDateFormat} + * @throws IllegalArgumentException 日期格式问题 + */ + public static FastDateFormat getInstance(final String pattern) { + return cache.getInstance(pattern, null, null); + } + + /** + * 获得 {@link FastDateFormat} 实例
+ * 支持缓存 + * + * @param pattern 使用{@link java.text.SimpleDateFormat} 相同的日期格式 + * @param timeZone 时区{@link TimeZone} + * @return {@link FastDateFormat} + * @throws IllegalArgumentException 日期格式问题 + */ + public static FastDateFormat getInstance(final String pattern, final TimeZone timeZone) { + return cache.getInstance(pattern, timeZone, null); + } + + /** + * 获得 {@link FastDateFormat} 实例
+ * 支持缓存 + * + * @param pattern 使用{@link java.text.SimpleDateFormat} 相同的日期格式 + * @param locale {@link Locale} 日期地理位置 + * @return {@link FastDateFormat} + * @throws IllegalArgumentException 日期格式问题 + */ + public static FastDateFormat getInstance(final String pattern, final Locale locale) { + return cache.getInstance(pattern, null, locale); + } + + /** + * 获得 {@link FastDateFormat} 实例
+ * 支持缓存 + * + * @param pattern 使用{@link java.text.SimpleDateFormat} 相同的日期格式 + * @param timeZone 时区{@link TimeZone} + * @param locale {@link Locale} 日期地理位置 + * @return {@link FastDateFormat} + * @throws IllegalArgumentException 日期格式问题 + */ + public static FastDateFormat getInstance(final String pattern, final TimeZone timeZone, final Locale locale) { + return cache.getInstance(pattern, timeZone, locale); + } + + // ----------------------------------------------------------------------- + /** + * 获得 {@link FastDateFormat} 实例
+ * 支持缓存 + * + * @param style date style: FULL, LONG, MEDIUM, or SHORT + * @return 本地化 {@link FastDateFormat} + */ + public static FastDateFormat getDateInstance(final int style) { + return cache.getDateInstance(style, null, null); + } + + /** + * 获得 {@link FastDateFormat} 实例
+ * 支持缓存 + * + * @param style date style: FULL, LONG, MEDIUM, or SHORT + * @param locale {@link Locale} 日期地理位置 + * @return 本地化 {@link FastDateFormat} + */ + public static FastDateFormat getDateInstance(final int style, final Locale locale) { + return cache.getDateInstance(style, null, locale); + } + + /** + * 获得 {@link FastDateFormat} 实例
+ * 支持缓存 + * + * @param style date style: FULL, LONG, MEDIUM, or SHORT + * @param timeZone 时区{@link TimeZone} + * @return 本地化 {@link FastDateFormat} + */ + public static FastDateFormat getDateInstance(final int style, final TimeZone timeZone) { + return cache.getDateInstance(style, timeZone, null); + } + + /** + * 获得 {@link FastDateFormat} 实例
+ * 支持缓存 + * + * @param style date style: FULL, LONG, MEDIUM, or SHORT + * @param timeZone 时区{@link TimeZone} + * @param locale {@link Locale} 日期地理位置 + * @return 本地化 {@link FastDateFormat} + */ + public static FastDateFormat getDateInstance(final int style, final TimeZone timeZone, final Locale locale) { + return cache.getDateInstance(style, timeZone, locale); + } + + // ----------------------------------------------------------------------- + /** + * 获得 {@link FastDateFormat} 实例
+ * 支持缓存 + * + * @param style time style: FULL, LONG, MEDIUM, or SHORT + * @return 本地化 {@link FastDateFormat} + */ + public static FastDateFormat getTimeInstance(final int style) { + return cache.getTimeInstance(style, null, null); + } + + /** + * 获得 {@link FastDateFormat} 实例
+ * 支持缓存 + * + * @param style time style: FULL, LONG, MEDIUM, or SHORT + * @param locale {@link Locale} 日期地理位置 + * @return 本地化 {@link FastDateFormat} + */ + public static FastDateFormat getTimeInstance(final int style, final Locale locale) { + return cache.getTimeInstance(style, null, locale); + } + + /** + * 获得 {@link FastDateFormat} 实例
+ * 支持缓存 + * + * @param style time style: FULL, LONG, MEDIUM, or SHORT + * @param timeZone optional time zone, overrides time zone of formatted time + * @return 本地化 {@link FastDateFormat} + */ + public static FastDateFormat getTimeInstance(final int style, final TimeZone timeZone) { + return cache.getTimeInstance(style, timeZone, null); + } + + /** + * 获得 {@link 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 本地化 {@link FastDateFormat} + */ + public static FastDateFormat getTimeInstance(final int style, final TimeZone timeZone, final Locale locale) { + return cache.getTimeInstance(style, timeZone, locale); + } + + // ----------------------------------------------------------------------- + /** + * 获得 {@link FastDateFormat} 实例
+ * 支持缓存 + * + * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT + * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT + * @return 本地化 {@link FastDateFormat} + */ + public static FastDateFormat getDateTimeInstance(final int dateStyle, final int timeStyle) { + return cache.getDateTimeInstance(dateStyle, timeStyle, null, null); + } + + /** + * 获得 {@link FastDateFormat} 实例
+ * 支持缓存 + * + * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT + * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT + * @param locale {@link Locale} 日期地理位置 + * @return 本地化 {@link FastDateFormat} + */ + public static FastDateFormat getDateTimeInstance(final int dateStyle, final int timeStyle, final Locale locale) { + return cache.getDateTimeInstance(dateStyle, timeStyle, null, locale); + } + + /** + * 获得 {@link FastDateFormat} 实例
+ * 支持缓存 + * + * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT + * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT + * @param timeZone 时区{@link TimeZone} + * @return 本地化 {@link FastDateFormat} + */ + public static FastDateFormat getDateTimeInstance(final int dateStyle, final int timeStyle, final TimeZone timeZone) { + return getDateTimeInstance(dateStyle, timeStyle, timeZone, null); + } + + /** + * 获得 {@link 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 本地化 {@link 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(); + } + + // Basics + // ----------------------------------------------------------------------- + @Override + public boolean equals(final Object obj) { + if (obj instanceof FastDateFormat == false) { + 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/hutool-core/src/main/java/cn/hutool/core/date/format/FastDateParser.java b/hutool-core/src/main/java/cn/hutool/core/date/format/FastDateParser.java new file mode 100644 index 000000000..81029559f --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/date/format/FastDateParser.java @@ -0,0 +1,834 @@ +package cn.hutool.core.date.format; + +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.Set; +import java.util.TimeZone; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +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 + * + * @since 2.16.2 + * @see FastDatePrinter + */ +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 = new Comparator(){ + @Override + public int compare(final String left, final String right) { + return right.compareTo(left); + } + }; + + /** + *

+ * 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 + */ + protected FastDateParser(final String pattern, final TimeZone timeZone, final 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 + */ + protected 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 java.util.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 Object parseObject(final String source) throws ParseException { + return parse(source); + } + + @Override + public Date parse(final 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 Object parseObject(final String source, final ParsePosition pos) { + return parse(source, pos); + } + + @Override + public Date parse(final String source, final 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(final String source, final ParsePosition pos, final 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 formatField A sub-sequence of the SimpleDateFormat pattern + * @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 ConcurrentHashMap<>(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; + } + + /** + * {@inheritDoc} + */ + @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); + } + + /** + * {@inheritDoc} + */ + @Override + void setCalendar(final FastDateParser parser, final Calendar cal, final String value) { + final Integer iVal = lKeyValues.get(value.toLowerCase(locale)); + cal.set(field, iVal.intValue()); + } + } + + /** + * 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; + } + + /** + * {@inheritDoc} + */ + @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){ + /** + * {@inheritDoc} + */ + @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 (tzId.equalsIgnoreCase("GMT")) { + 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); + } + + /** + * {@inheritDoc} + */ + @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); + cal.set(Calendar.ZONE_OFFSET, tzInfo.zone.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); + } + + /** + * {@inheritDoc} + */ + @Override + void setCalendar(final FastDateParser parser, final Calendar cal, final String value) { + if (value.equals("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/hutool-core/src/main/java/cn/hutool/core/date/format/FastDatePrinter.java b/hutool-core/src/main/java/cn/hutool/core/date/format/FastDatePrinter.java new file mode 100644 index 000000000..1a4548365 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/date/format/FastDatePrinter.java @@ -0,0 +1,1327 @@ +package cn.hutool.core.date.format; + +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.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import cn.hutool.core.date.DateException; + +/** + * {@link java.text.SimpleDateFormat} 的线程安全版本,用于将 {@link Date} 格式化输出
+ * Thanks to Apache Commons Lang 3.5 + * + * @since 2.16.2 + * @see FastDateParser + */ +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} 日期地理位置 + */ + protected FastDatePrinter(final String pattern, final TimeZone timeZone, final Locale locale) { + super(pattern, timeZone, locale); + init(); + } + + /** + * 初始化 + */ + private void init() { + final List rulesList = parsePattern(); + rules = rulesList.toArray(new Rule[rulesList.size()]); + + 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, tokenLen < 4 ? 4 : tokenLen); + } + 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(final String pattern, final 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(final int field, final 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(final 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(final long millis) { + final Calendar c = Calendar.getInstance(timeZone, locale); + c.setTimeInMillis(millis); + return applyRulesToString(c); + } + + @Override + public String format(final Date date) { + final Calendar c = Calendar.getInstance(timeZone, locale); + c.setTime(date); + return applyRulesToString(c); + } + + @Override + public String format(final Calendar calendar) { + return format(calendar, new StringBuilder(mMaxLengthEstimate)).toString(); + } + + @Override + public B format(final long millis, final B buf) { + final Calendar c = Calendar.getInstance(timeZone, locale); + c.setTimeInMillis(millis); + return applyRules(c, buf); + } + + @Override + public B format(final Date date, final B buf) { + final Calendar c = Calendar.getInstance(timeZone, locale); + c.setTime(date); + return applyRules(c, buf); + } + + @Override + public B format(Calendar calendar, final 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(final 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(final Calendar calendar, final 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(final 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(final Appendable buffer, final 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(final 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(final String value) { + mValue = value; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return mValue.length(); + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final 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(final int field, final 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(final Appendable buffer, final 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(final int field) { + mField = field; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return 4; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + appendTo(buffer, calendar.get(mField)); + } + + /** + * {@inheritDoc} + */ + @Override + public final void appendTo(final Appendable buffer, final 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() { + super(); + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return 2; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + appendTo(buffer, calendar.get(Calendar.MONTH) + 1); + } + + /** + * {@inheritDoc} + */ + @Override + public final void appendTo(final Appendable buffer, final 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(final int field, final 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(final Appendable buffer, final Calendar calendar) throws IOException { + appendTo(buffer, calendar.get(mField)); + } + + /** + * {@inheritDoc} + */ + @Override + public final void appendTo(final Appendable buffer, final 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(final int field) { + mField = field; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return 2; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + appendTo(buffer, calendar.get(mField)); + } + + /** + * {@inheritDoc} + */ + @Override + public final void appendTo(final Appendable buffer, final 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() { + super(); + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return 2; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + appendTo(buffer, calendar.get(Calendar.YEAR) % 100); + } + + /** + * {@inheritDoc} + */ + @Override + public final void appendTo(final Appendable buffer, final 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() { + super(); + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return 2; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + appendTo(buffer, calendar.get(Calendar.MONTH) + 1); + } + + /** + * {@inheritDoc} + */ + @Override + public final void appendTo(final Appendable buffer, final 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(final Appendable buffer, final 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(final Appendable buffer, final 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(final NumberRule rule) { + mRule = rule; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return mRule.estimateLength(); + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final 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(final Appendable buffer, final 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(final NumberRule rule) { + mRule = rule; + } + + @Override + public int estimateLength() { + return mRule.estimateLength(); + } + + @Override + public void appendTo(final Appendable buffer, final 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(final Appendable buffer, final 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(final Appendable buffer, final Calendar calendar) throws IOException { + mRule.appendTo(buffer, calendar.getWeekYear()); + } + + @Override + public void appendTo(final Appendable buffer, final int value) throws IOException { + mRule.appendTo(buffer, value); + } + } + + // ----------------------------------------------------------------------- + + private static final ConcurrentMap cTimeZoneDisplayCache = new ConcurrentHashMap<>(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(final TimeZone tz, final boolean daylight, final int style, final Locale locale) { + final TimeZoneDisplayKey key = new TimeZoneDisplayKey(tz, daylight, style, locale); + String value = cTimeZoneDisplayCache.get(key); + if (value == null) { + // This is a very slow call, so cache the results. + value = tz.getDisplayName(daylight, style, locale); + final String prior = cTimeZoneDisplayCache.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(final TimeZone timeZone, final Locale locale, final 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(final Appendable buffer, final 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(final boolean colon) { + mColon = colon; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return 5; + } + + /** + * {@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('-'); + 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(final 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(final 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/hutool-core/src/main/java/cn/hutool/core/date/format/FormatCache.java b/hutool-core/src/main/java/cn/hutool/core/date/format/FormatCache.java new file mode 100644 index 000000000..cbdfd4781 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/date/format/FormatCache.java @@ -0,0 +1,252 @@ +package cn.hutool.core.date.format; + +import java.text.DateFormat; +import java.text.Format; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Locale; +import java.util.TimeZone; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import cn.hutool.core.lang.Assert; + +/** + * 日期格式化器缓存
+ * 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 ConcurrentHashMap<>(7); + + private static final ConcurrentMap cDateTimeInstanceCache = new ConcurrentHashMap<>(7); + + /** + * 使用默认的pattern、timezone和locale获得缓存中的实例 + * @return a date/time formatter + */ + public F getInstance() { + return getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, TimeZone.getDefault(), Locale.getDefault()); + } + + /** + * 使用 pattern, time zone and locale 获得对应的 格式化器 + * + * @param pattern 非空日期格式,使用与 {@link java.text.SimpleDateFormat}相同格式 + * @param timeZone 时区,默认当前时区 + * @param locale 地区,默认使用当前地区 + * @return 格式化器 + * @throws IllegalArgumentException pattern 无效或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 MultipartKey key = new MultipartKey(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 java.text.SimpleDateFormat}相同格式 + * @param timeZone 时区,默认当前时区 + * @param locale 地区,默认使用当前地区 + * @return 格式化器 + * @throws IllegalArgumentException pattern 无效或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 + private 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/time formatter instance using the specified style, time zone and locale. + *

+ * + * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT + * @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 getDateTimeInstance(final int dateStyle, final int timeStyle, final TimeZone timeZone, final Locale locale) { + return getDateTimeInstance(Integer.valueOf(dateStyle), Integer.valueOf(timeStyle), 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(Integer.valueOf(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, Integer.valueOf(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 MultipartKey key = new MultipartKey(dateStyle, timeStyle, locale); + + String pattern = cDateTimeInstanceCache.get(key); + if (pattern == null) { + try { + DateFormat formatter; + if (dateStyle == null) { + formatter = DateFormat.getTimeInstance(timeStyle.intValue(), locale); + } else if (timeStyle == null) { + formatter = DateFormat.getDateInstance(dateStyle.intValue(), locale); + } else { + formatter = DateFormat.getDateTimeInstance(dateStyle.intValue(), timeStyle.intValue(), locale); + } + pattern = ((SimpleDateFormat) formatter).toPattern(); + final String previous = cDateTimeInstanceCache.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; + } + + // ---------------------------------------------------------------------- + /** + *

+ * Helper class to hold multi-part Map keys + *

+ */ + private static class MultipartKey { + private final Object[] keys; + private int hashCode; + + /** + * Constructs an instance of MultipartKey to hold the specified objects. + * + * @param keys the set of objects that make up the key. Each key may be null. + */ + public MultipartKey(final Object... keys) { + this.keys = keys; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final MultipartKey other = (MultipartKey) obj; + if (false == Arrays.equals(keys, other.keys)) { + return false; + } + return true; + } + + + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + if (hashCode == 0) { + int rc = 0; + for (final Object key : keys) { + if (key != null) { + rc = rc * 7 + key.hashCode(); + } + } + hashCode = rc; + } + return hashCode; + } + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/date/format/package-info.java b/hutool-core/src/main/java/cn/hutool/core/date/format/package-info.java new file mode 100644 index 000000000..cf4868b23 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/date/package-info.java b/hutool-core/src/main/java/cn/hutool/core/date/package-info.java new file mode 100644 index 000000000..7eb924ef6 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/exceptions/DependencyException.java b/hutool-core/src/main/java/cn/hutool/core/exceptions/DependencyException.java new file mode 100644 index 000000000..c1c99c455 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/exceptions/DependencyException.java @@ -0,0 +1,33 @@ +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(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/exceptions/ExceptionUtil.java b/hutool-core/src/main/java/cn/hutool/core/exceptions/ExceptionUtil.java new file mode 100644 index 000000000..283ed7b34 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/exceptions/ExceptionUtil.java @@ -0,0 +1,384 @@ +package cn.hutool.core.exceptions; + +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; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.io.FastByteArrayOutputStream; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 异常工具类 + * + * @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 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); + } + + /** + * 剥离反射引发的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(); + } + + /** + * 获取指定层的堆栈信息 + * + * @return 指定层的堆栈信息 + * @since 4.1.4 + */ + public static StackTraceElement getStackElement(int i) { + return getStackElements()[i]; + } + + /** + * 获取入口堆栈信息 + * + * @return 入口堆栈信息 + * @since 4.1.4 + */ + public static StackTraceElement getRootStackElement() { + final StackTraceElement[] stackElements = getStackElements(); + return stackElements[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 限制最大长度 + * @param replaceCharToStrMap 替换字符为指定字符串 + * @return 堆栈转为的字符串 + */ + public static String stacktraceToString(Throwable throwable, int limit, Map replaceCharToStrMap) { + final FastByteArrayOutputStream baos = new FastByteArrayOutputStream(); + throwable.printStackTrace(new PrintStream(baos)); + String exceptionStr = baos.toString(); + int length = exceptionStr.length(); + if (limit > 0 && limit < length) { + length = limit; + } + + if (CollectionUtil.isNotEmpty(replaceCharToStrMap)) { + final StringBuilder sb = StrUtil.builder(); + char c; + String value; + for (int i = 0; i < length; i++) { + c = exceptionStr.charAt(i); + value = replaceCharToStrMap.get(c); + if (null != value) { + sb.append(value); + } else { + sb.append(c); + } + } + return sb.toString(); + } else { + 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 && false == 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/hutool-core/src/main/java/cn/hutool/core/exceptions/NotInitedException.java b/hutool-core/src/main/java/cn/hutool/core/exceptions/NotInitedException.java new file mode 100644 index 000000000..211ffbf3a --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/exceptions/NotInitedException.java @@ -0,0 +1,32 @@ +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(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/exceptions/StatefulException.java b/hutool-core/src/main/java/cn/hutool/core/exceptions/StatefulException.java new file mode 100644 index 000000000..a4e919c3b --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/exceptions/StatefulException.java @@ -0,0 +1,57 @@ +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(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/hutool-core/src/main/java/cn/hutool/core/exceptions/UtilException.java b/hutool-core/src/main/java/cn/hutool/core/exceptions/UtilException.java new file mode 100644 index 000000000..eef17aafd --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/exceptions/UtilException.java @@ -0,0 +1,31 @@ +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(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/exceptions/ValidateException.java b/hutool-core/src/main/java/cn/hutool/core/exceptions/ValidateException.java new file mode 100644 index 000000000..5251be108 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/exceptions/ValidateException.java @@ -0,0 +1,43 @@ +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(int status, String msg, Throwable throwable) { + super(status, msg, throwable); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/exceptions/package-info.java b/hutool-core/src/main/java/cn/hutool/core/exceptions/package-info.java new file mode 100644 index 000000000..a2bfa53f3 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/getter/ArrayTypeGetter.java b/hutool-core/src/main/java/cn/hutool/core/getter/ArrayTypeGetter.java new file mode 100644 index 000000000..edc99d4ea --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/getter/BasicTypeGetter.java b/hutool-core/src/main/java/cn/hutool/core/getter/BasicTypeGetter.java new file mode 100644 index 000000000..a4c2256cc --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/getter/BasicTypeGetter.java @@ -0,0 +1,127 @@ +package cn.hutool.core.getter; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Date; + +/** + * 基本类型的getter接口
+ * 提供一个统一的接口定义返回不同类型的值(基本类型)
+ * @author Looly + */ +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/hutool-core/src/main/java/cn/hutool/core/getter/GroupedTypeGetter.java b/hutool-core/src/main/java/cn/hutool/core/getter/GroupedTypeGetter.java new file mode 100644 index 000000000..f83ef3afb --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/getter/ListTypeGetter.java b/hutool-core/src/main/java/cn/hutool/core/getter/ListTypeGetter.java new file mode 100644 index 000000000..dd7ff9d74 --- /dev/null +++ b/hutool-core/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); + + /** + * 获取BigDecimal型属性值列表 + * + * @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/hutool-core/src/main/java/cn/hutool/core/getter/OptArrayTypeGetter.java b/hutool-core/src/main/java/cn/hutool/core/getter/OptArrayTypeGetter.java new file mode 100644 index 000000000..24be29a85 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/getter/OptBasicTypeGetter.java b/hutool-core/src/main/java/cn/hutool/core/getter/OptBasicTypeGetter.java new file mode 100644 index 000000000..3203d79cb --- /dev/null +++ b/hutool-core/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 + */ + public > 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/hutool-core/src/main/java/cn/hutool/core/getter/OptNullBasicTypeFromObjectGetter.java b/hutool-core/src/main/java/cn/hutool/core/getter/OptNullBasicTypeFromObjectGetter.java new file mode 100644 index 000000000..07cdcbc90 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/getter/OptNullBasicTypeFromObjectGetter.java @@ -0,0 +1,136 @@ +package cn.hutool.core.getter; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Date; + +import cn.hutool.core.convert.Convert; + +/** + * 基本类型的getter接口抽象实现,所有类型的值获取都是通过将getObj获得的值转换而来
+ * 用户只需实现getObj方法即可,其他类型将会从Object结果中转换 + * 在不提供默认值的情况下, 如果值不存在或获取错误,返回null
+ * @author Looly + */ +public abstract class OptNullBasicTypeFromObjectGetter extends OptNullBasicTypeGetter{ + + @Override + public abstract Object getObj(K key, Object defaultValue); + + @Override + public String getStr(K key, String defaultValue) { + final Object obj = getObj(key); + if(null == obj) { + return defaultValue; + } + return Convert.toStr(obj, defaultValue); + } + + @Override + public Integer getInt(K key, Integer defaultValue) { + final Object obj = getObj(key); + if(null == obj) { + return defaultValue; + } + return Convert.toInt(obj, defaultValue); + } + + @Override + public Short getShort(K key, Short defaultValue) { + final Object obj = getObj(key); + if(null == obj) { + return defaultValue; + } + return Convert.toShort(obj, defaultValue); + } + + @Override + public Boolean getBool(K key, Boolean defaultValue) { + final Object obj = getObj(key); + if(null == obj) { + return defaultValue; + } + return Convert.toBool(obj, defaultValue); + } + + @Override + public Long getLong(K key, Long defaultValue) { + final Object obj = getObj(key); + if(null == obj) { + return defaultValue; + } + return Convert.toLong(obj, defaultValue); + } + + @Override + public Character getChar(K key, Character defaultValue) { + final Object obj = getObj(key); + if(null == obj) { + return defaultValue; + } + return Convert.toChar(obj, defaultValue); + } + + @Override + public Float getFloat(K key, Float defaultValue) { + final Object obj = getObj(key); + if(null == obj) { + return defaultValue; + } + return Convert.toFloat(obj, defaultValue); + } + + @Override + public Double getDouble(K key, Double defaultValue) { + final Object obj = getObj(key); + if(null == obj) { + return defaultValue; + } + return Convert.toDouble(obj, defaultValue); + } + + @Override + public Byte getByte(K key, Byte defaultValue) { + final Object obj = getObj(key); + if(null == obj) { + return defaultValue; + } + return Convert.toByte(obj, defaultValue); + } + + @Override + public BigDecimal getBigDecimal(K key, BigDecimal defaultValue) { + final Object obj = getObj(key); + if(null == obj) { + return defaultValue; + } + return Convert.toBigDecimal(obj, defaultValue); + } + + @Override + public BigInteger getBigInteger(K key, BigInteger defaultValue) { + final Object obj = getObj(key); + if(null == obj) { + return defaultValue; + } + return Convert.toBigInteger(obj, defaultValue); + } + + @Override + public > 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 + public Date getDate(K key, Date defaultValue) { + final Object obj = getObj(key); + if(null == obj) { + return defaultValue; + } + return Convert.toDate(obj, defaultValue); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/getter/OptNullBasicTypeFromStringGetter.java b/hutool-core/src/main/java/cn/hutool/core/getter/OptNullBasicTypeFromStringGetter.java new file mode 100644 index 000000000..b8d6aedad --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/getter/OptNullBasicTypeFromStringGetter.java @@ -0,0 +1,84 @@ +package cn.hutool.core.getter; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Date; + +import cn.hutool.core.convert.Convert; + +/** + * 基本类型的getter接口抽象实现,所有类型的值获取都是通过将String转换而来
+ * 用户只需实现getStr方法即可,其他类型将会从String结果中转换 在不提供默认值的情况下, 如果值不存在或获取错误,返回null
+ * + * @author Looly + */ +public abstract class OptNullBasicTypeFromStringGetter extends OptNullBasicTypeGetter { + + @Override + public abstract String getStr(K key, String defaultValue); + + @Override + public Object getObj(K key, Object defaultValue) { + return getStr(key, null == defaultValue ? null : defaultValue.toString()); + } + + @Override + public Integer getInt(K key, Integer defaultValue) { + return Convert.toInt(getStr(key), defaultValue); + } + + @Override + public Short getShort(K key, Short defaultValue) { + return Convert.toShort(getStr(key), defaultValue); + } + + @Override + public Boolean getBool(K key, Boolean defaultValue) { + return Convert.toBool(getStr(key), defaultValue); + } + + @Override + public Long getLong(K key, Long defaultValue) { + return Convert.toLong(getStr(key), defaultValue); + } + + @Override + public Character getChar(K key, Character defaultValue) { + return Convert.toChar(getStr(key), defaultValue); + } + + @Override + public Float getFloat(K key, Float defaultValue) { + return Convert.toFloat(getStr(key), defaultValue); + } + + @Override + public Double getDouble(K key, Double defaultValue) { + return Convert.toDouble(getStr(key), defaultValue); + } + + @Override + public Byte getByte(K key, Byte defaultValue) { + return Convert.toByte(getStr(key), defaultValue); + } + + @Override + public BigDecimal getBigDecimal(K key, BigDecimal defaultValue) { + return Convert.toBigDecimal(getStr(key), defaultValue); + } + + @Override + public BigInteger getBigInteger(K key, BigInteger defaultValue) { + return Convert.toBigInteger(getStr(key), defaultValue); + } + + @Override + public > E getEnum(Class clazz, K key, E defaultValue) { + return Convert.toEnum(clazz, getStr(key), defaultValue); + } + + @Override + public Date getDate(K key, Date defaultValue) { + return Convert.toDate(getStr(key), defaultValue); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/getter/OptNullBasicTypeGetter.java b/hutool-core/src/main/java/cn/hutool/core/getter/OptNullBasicTypeGetter.java new file mode 100644 index 000000000..45b40f6a6 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/getter/OptNullBasicTypeGetter.java @@ -0,0 +1,177 @@ +package cn.hutool.core.getter; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Date; + +/** + * 基本类型的getter接口抽象实现
+ * 提供一个统一的接口定义返回不同类型的值(基本类型)
+ * 在不提供默认值的情况下, 如果值不存在或获取错误,返回null
+ * 用户只需实现{@code com.xiaoleilu.hutool.getter.OptBasicTypeGetter}接口即可 + * @author Looly + */ +public abstract class OptNullBasicTypeGetter implements BasicTypeGetter, OptBasicTypeGetter{ + + @Override + public Object getObj(K key) { + return getObj(key, null); + } + + /** + * 获取字符串型属性值
+ * 无值或获取错误返回null + * + * @param key 属性名 + * @return 属性值 + */ + @Override + public String getStr(K key){ + return this.getStr(key, null); + } + + /** + * 获取int型属性值
+ * 无值或获取错误返回null + * + * @param key 属性名 + * @return 属性值 + */ + @Override + public Integer getInt(K key) { + return this.getInt(key, null); + } + + /** + * 获取short型属性值
+ * 无值或获取错误返回null + * + * @param key 属性名 + * @return 属性值 + */ + @Override + public Short getShort(K key){ + return this.getShort(key, null); + } + + /** + * 获取boolean型属性值
+ * 无值或获取错误返回null + * + * @param key 属性名 + * @return 属性值 + */ + @Override + public Boolean getBool(K key){ + return this.getBool(key, null); + } + + /** + * 获取long型属性值
+ * 无值或获取错误返回null + * + * @param key 属性名 + * @return 属性值 + */ + @Override + public Long getLong(K key){ + return this.getLong(key, null); + } + + /** + * 获取char型属性值
+ * 无值或获取错误返回null + * + * @param key 属性名 + * @return 属性值 + */ + @Override + public Character getChar(K key){ + return this.getChar(key, null); + } + + /** + * 获取float型属性值
+ * 无值或获取错误返回null + * + * @param key 属性名 + * @return 属性值 + */ + @Override + public Float getFloat(K key){ + return this.getFloat(key, null); + } + + /** + * 获取double型属性值
+ * 无值或获取错误返回null + * + * @param key 属性名 + * @return 属性值 + */ + @Override + public Double getDouble(K key){ + return this.getDouble(key, null); + } + + /** + * 获取byte型属性值
+ * 无值或获取错误返回null + * + * @param key 属性名 + * @return 属性值 + */ + @Override + public Byte getByte(K key){ + return this.getByte(key, null); + } + + /** + * 获取BigDecimal型属性值
+ * 无值或获取错误返回null + * + * @param key 属性名 + * @return 属性值 + */ + @Override + public BigDecimal getBigDecimal(K key){ + return this.getBigDecimal(key, null); + } + + /** + * 获取BigInteger型属性值
+ * 无值或获取错误返回null + * + * @param key 属性名 + * @return 属性值 + */ + @Override + public BigInteger getBigInteger(K key){ + return this.getBigInteger(key, null); + } + + /** + * 获取Enum型属性值
+ * 无值或获取错误返回null + * + * @param clazz Enum 的 Class + * @param key 属性名 + * @return 属性值 + */ + @Override + public > E getEnum(Class clazz, K key) { + return this.getEnum(clazz, key, null); + } + + /** + * 获取Date型属性值
+ * 无值或获取错误返回null + * + * @param key 属性名 + * @return 属性值 + */ + @Override + public Date getDate(K key) { + return this.getDate(key, null); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/getter/package-info.java b/hutool-core/src/main/java/cn/hutool/core/getter/package-info.java new file mode 100644 index 000000000..754b27b47 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/img/GraphicsUtil.java b/hutool-core/src/main/java/cn/hutool/core/img/GraphicsUtil.java new file mode 100644 index 000000000..f1ce6a002 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/img/GraphicsUtil.java @@ -0,0 +1,118 @@ +package cn.hutool.core.img; + +import java.awt.Color; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.image.BufferedImage; + +/** + * {@link Graphics}相关工具类 + * + * @author looly + * @since 4.5.2 + */ +public class GraphicsUtil { + + /** + * 创建{@link Graphics2D} + * + * @param image {@link BufferedImage} + * @param color {@link Color}背景颜色以及当前画笔颜色,{@code null}表示不设置背景色 + * @return {@link Graphics2D} + * @since 4.5.2 + */ + public static Graphics2D createGraphics(BufferedImage image, Color color) { + final Graphics2D g = image.createGraphics(); + + if(null != color) { + // 填充背景 + g.setColor(color); + g.fillRect(0, 0, image.getWidth(), image.getHeight()); + } + + return g; + } + + /** + * 获取文字居中高度的Y坐标(距离上边距距离)
+ * 此方法依赖FontMetrics,如果获取失败,默认为背景高度的1/3 + * + * @param g {@link Graphics2D}画笔 + * @param backgroundHeight 背景高度 + * @return 最小高度,-1表示无法获取 + * @since 4.5.17 + */ + public static int getCenterY(Graphics g, int backgroundHeight) { + // 获取允许文字最小高度 + FontMetrics metrics = null; + try { + metrics = g.getFontMetrics(); + } catch (Exception e) { + // 此处报告bug某些情况下会抛出IndexOutOfBoundsException,在此做容错处理 + } + int y; + if (null != metrics) { + y = (backgroundHeight - metrics.getHeight()) / 2 + metrics.getAscent(); + } else { + y = backgroundHeight / 3; + } + return y; + } + + /** + * 绘制字符串,使用随机颜色,默认抗锯齿 + * + * @param g {@link Graphics}画笔 + * @param str 字符串 + * @param font 字体 + * @param width 字符串总宽度 + * @param height 字符串背景高度 + * @return 画笔对象 + * @since 4.5.10 + */ + public static Graphics drawStringColourful(Graphics g, String str, Font font, int width, int height) { + return drawString(g, str, font, null, width, height); + } + + /** + * 绘制字符串,默认抗锯齿 + * + * @param g {@link Graphics}画笔 + * @param str 字符串 + * @param font 字体 + * @param color 字体颜色,{@code null} 表示使用随机颜色(每个字符单独随机) + * @param width 字符串背景的宽度 + * @param height 字符串背景的高度 + * @return 画笔对象 + * @since 4.5.10 + */ + public static Graphics drawString(Graphics g, String str, Font font, Color color, int width, int height) { + // 抗锯齿 + if (g instanceof Graphics2D) { + ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + } + // 创建字体 + g.setFont(font); + + // 文字高度(必须在设置字体后调用) + int midY = GraphicsUtil.getCenterY(g, height); + if (null != color) { + g.setColor(color); + } + + final int len = str.length(); + int charWidth = width / len; + for (int i = 0; i < len; i++) { + if (null == color) { + // 产生随机的颜色值,让输出的每个字符的颜色值都将不同。 + g.setColor(ImgUtil.randomColor()); + } + g.drawString(String.valueOf(str.charAt(i)), i * charWidth, midY); + } + return g; + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/img/Img.java b/hutool-core/src/main/java/cn/hutool/core/img/Img.java new file mode 100644 index 000000000..246846f80 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/img/Img.java @@ -0,0 +1,684 @@ +package cn.hutool.core.img; + +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.Toolkit; +import java.awt.color.ColorSpace; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.RoundRectangle2D; +import java.awt.image.AffineTransformOp; +import java.awt.image.BufferedImage; +import java.awt.image.ColorConvertOp; +import java.awt.image.CropImageFilter; +import java.awt.image.FilteredImageSource; +import java.awt.image.ImageFilter; +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Serializable; +import java.net.URL; +import java.nio.file.Path; + +import javax.imageio.ImageIO; +import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.ImageOutputStream; + +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.lang.Assert; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 图像编辑器 + * + * @author looly + * @since 4.1.5 + */ +public class Img implements Serializable{ + private static final long serialVersionUID = 1L; + + private BufferedImage srcImage; + private Image targetImage; + /** 目标图片文件格式,用于写出 */ + private String targetImageType = ImgUtil.IMAGE_TYPE_JPG; + /** 计算x,y坐标的时候是否从中心做为原始坐标开始计算 */ + private boolean positionBaseCentre = true; + /** 图片输出质量,用于压缩 */ + private float quality = -1; + + /** + * 从Path读取图片并开始处理 + * + * @param imagePath 图片文件路径 + * @return {@link Img} + */ + public static Img from(Path imagePath) { + return from(imagePath.toFile()); + } + + /** + * 从文件读取图片并开始处理 + * + * @param imageFile 图片文件 + * @return {@link Img} + */ + public static Img from(File imageFile) { + return new Img(ImgUtil.read(imageFile)); + } + + /** + * 从资源对象中读取图片并开始处理 + * + * @param resource 图片资源对象 + * @return {@link Img} + * @since 4.4.1 + */ + public static Img from(Resource resource) { + return from(resource.getStream()); + } + + /** + * 从流读取图片并开始处理 + * + * @param in 图片流 + * @return {@link Img} + */ + public static Img from(InputStream in) { + return new Img(ImgUtil.read(in)); + } + + /** + * 从ImageInputStream取图片并开始处理 + * + * @param imageStream 图片流 + * @return {@link Img} + */ + public static Img from(ImageInputStream imageStream) { + return new Img(ImgUtil.read(imageStream)); + } + + /** + * 从URL取图片并开始处理 + * + * @param imageUrl 图片URL + * @return {@link Img} + */ + public static Img from(URL imageUrl) { + return new Img(ImgUtil.read(imageUrl)); + } + + /** + * 从Image取图片并开始处理 + * + * @param image 图片 + * @return {@link Img} + */ + public static Img from(Image image) { + return new Img(ImgUtil.toBufferedImage(image)); + } + + /** + * 构造 + * + * @param srcImage 来源图片 + */ + public Img(BufferedImage srcImage) { + this.srcImage = srcImage; + } + + /** + * 设置目标图片文件格式,用于写出 + * + * @param imgType 图片格式 + * @return this + * @see ImgUtil#IMAGE_TYPE_JPG + * @see ImgUtil#IMAGE_TYPE_PNG + */ + public Img setTargetImageType(String imgType) { + this.targetImageType = imgType; + return this; + } + + /** + * 计算x,y坐标的时候是否从中心做为原始坐标开始计算 + * + * @param positionBaseCentre 是否从中心做为原始坐标开始计算 + * @since 4.1.15 + */ + public Img setPositionBaseCentre(boolean positionBaseCentre) { + this.positionBaseCentre = positionBaseCentre; + return this; + } + + /** + * 设置图片输出质量,数字为0~1(不包括0和1)表示质量压缩比,除此数字外设置表示不压缩 + * + * @param quality 质量,数字为0~1(不包括0和1)表示质量压缩比,除此数字外设置表示不压缩 + * @since 4.3.2 + */ + public Img setQuality(double quality) { + return setQuality((float) quality); + } + + /** + * 设置图片输出质量,数字为0~1(不包括0和1)表示质量压缩比,除此数字外设置表示不压缩 + * + * @param quality 质量,数字为0~1(不包括0和1)表示质量压缩比,除此数字外设置表示不压缩 + * @since 4.3.2 + */ + public Img setQuality(float quality) { + if (quality > 0 && quality < 1) { + this.quality = quality; + } else { + this.quality = 1; + } + return this; + } + + /** + * 缩放图像(按比例缩放) + * + * @param scale 缩放比例。比例大于1时为放大,小于1大于0为缩小 + * @return this + */ + public Img scale(float scale) { + if (scale < 0) { + // 自动修正负数 + scale = -scale; + } + + final Image srcImg = getValidSrcImg(); + + // PNG图片特殊处理 + if (ImgUtil.IMAGE_TYPE_PNG.equals(this.targetImageType)) { + final AffineTransformOp op = new AffineTransformOp(AffineTransform.getScaleInstance((double) scale, (double) scale), null); + this.targetImage = op.filter(ImgUtil.toBufferedImage(srcImg), null); + } else { + final String scaleStr = Float.toString(scale); + // 缩放后的图片宽 + int width = NumberUtil.mul(Integer.toString(srcImg.getWidth(null)), scaleStr).intValue(); + // 缩放后的图片高 + int height = NumberUtil.mul(Integer.toString(srcImg.getHeight(null)), scaleStr).intValue(); + scale(width, height); + } + return this; + } + + /** + * 缩放图像(按长宽缩放)
+ * 注意:目标长宽与原图不成比例会变形 + * + * @param width 目标宽度 + * @param height 目标高度 + * @return this + */ + public Img scale(int width, int height) { + final Image srcImg = getValidSrcImg(); + + int srcHeight = srcImg.getHeight(null); + int srcWidth = srcImg.getWidth(null); + int scaleType; + if (srcHeight == height && srcWidth == width) { + // 源与目标长宽一致返回原图 + this.targetImage = srcImg; + return this; + } else if (srcHeight < height || srcWidth < width) { + // 放大图片使用平滑模式 + scaleType = Image.SCALE_SMOOTH; + } else { + scaleType = Image.SCALE_DEFAULT; + } + + double sx = NumberUtil.div(width, srcWidth); + double sy = NumberUtil.div(height, srcHeight); + + if (ImgUtil.IMAGE_TYPE_PNG.equals(this.targetImageType)) { + final AffineTransformOp op = new AffineTransformOp(AffineTransform.getScaleInstance(sx, sy), null); + this.targetImage = op.filter(ImgUtil.toBufferedImage(srcImg), null); + } else { + this.targetImage = srcImg.getScaledInstance(width, height, scaleType); + } + + return this; + } + + /** + * 等比缩放图像,此方法按照按照给定的长宽等比缩放图片,按照长宽缩放比最多的一边等比缩放,空白部分填充背景色
+ * 缩放后默认为jpeg格式 + * + * @param width 缩放后的宽度 + * @param height 缩放后的高度 + * @param fixedColor 比例不对时补充的颜色,不补充为null + * @return this + */ + public Img scale(int width, int height, Color fixedColor) { + Image srcImage = getValidSrcImg(); + int srcHeight = srcImage.getHeight(null); + int srcWidth = srcImage.getWidth(null); + double heightRatio = NumberUtil.div(height, srcHeight); + double widthRatio = NumberUtil.div(width, srcWidth); + if (heightRatio == widthRatio) { + // 长宽都按照相同比例缩放时,返回缩放后的图片 + return scale(width, height); + } + + // 宽缩放比例多就按照宽缩放,否则按照高缩放 + if (widthRatio < heightRatio) { + scale(width, (int) (srcHeight * widthRatio)); + } else { + scale((int) (srcWidth * heightRatio), height); + } + + // 获取缩放后的新的宽和高 + srcImage = getValidSrcImg(); + srcHeight = srcImage.getHeight(null); + srcWidth = srcImage.getWidth(null); + + if (null == fixedColor) {// 补白 + fixedColor = Color.WHITE; + } + final BufferedImage image = new BufferedImage(width, height, getTypeInt()); + Graphics2D g = image.createGraphics(); + + // 设置背景 + g.setBackground(fixedColor); + g.clearRect(0, 0, width, height); + + // 在中间贴图 + g.drawImage(srcImage, (width - srcWidth) / 2, (height - srcHeight) / 2, srcWidth, srcHeight, fixedColor, null); + + g.dispose(); + this.targetImage = image; + return this; + } + + /** + * 图像切割(按指定起点坐标和宽高切割) + * + * @param rectangle 矩形对象,表示矩形区域的x,y,width,height + * @return this + */ + public Img cut(Rectangle rectangle) { + final Image srcImage = getValidSrcImg(); + rectangle = fixRectangle(rectangle, srcImage.getWidth(null), srcImage.getHeight(null)); + + final ImageFilter cropFilter = new CropImageFilter(rectangle.x, rectangle.y, rectangle.width, rectangle.height); + final Image image = Toolkit.getDefaultToolkit().createImage(new FilteredImageSource(srcImage.getSource(), cropFilter)); + this.targetImage = ImgUtil.toBufferedImage(image); + return this; + } + + /** + * 图像切割为圆形(按指定起点坐标和半径切割),填充满整个图片(直径取长宽最小值) + * + * @param x 原图的x坐标起始位置 + * @param y 原图的y坐标起始位置 + * @return this + * @since 4.1.15 + */ + public Img cut(int x, int y) { + return cut(x, y, -1); + } + + /** + * 图像切割为圆形(按指定起点坐标和半径切割) + * + * @param x 原图的x坐标起始位置 + * @param y 原图的y坐标起始位置 + * @param radius 半径,小于0表示填充满整个图片(直径取长宽最小值) + * @return this + * @since 4.1.15 + */ + public Img cut(int x, int y, int radius) { + final Image srcImage = getValidSrcImg(); + final int width = srcImage.getWidth(null); + final int height = srcImage.getHeight(null); + + // 计算直径 + final int diameter = radius > 0 ? radius * 2 : Math.min(width, height); + final BufferedImage targetImage = new BufferedImage(diameter, diameter, BufferedImage.TYPE_INT_ARGB); + final Graphics2D g = targetImage.createGraphics(); + g.setClip(new Ellipse2D.Double(0, 0, diameter, diameter)); + + if (this.positionBaseCentre) { + x = x - width / 2 + diameter / 2; + y = y - height / 2 + diameter / 2; + } + g.drawImage(srcImage, x, y, null); + g.dispose(); + this.targetImage = targetImage; + return this; + } + + /** + * 图片圆角处理 + * + * @param arc 圆角弧度,0~1,为长宽占比 + * @return this + * @since 4.5.3 + */ + public Img round(double arc) { + final Image srcImage = getValidSrcImg(); + final int width = srcImage.getWidth(null); + final int height = srcImage.getHeight(null); + + // 通过弧度占比计算弧度 + arc = NumberUtil.mul(arc, Math.min(width, height)); + + final BufferedImage targetImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + final Graphics2D g2 = targetImage.createGraphics(); + g2.setComposite(AlphaComposite.Src); + // 抗锯齿 + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2.fill(new RoundRectangle2D.Double(0, 0, width, height, arc, arc)); + g2.setComposite(AlphaComposite.SrcAtop); + g2.drawImage(srcImage, 0, 0, null); + g2.dispose(); + this.targetImage = targetImage; + return this; + } + + /** + * 彩色转为黑白 + * + * @return this + */ + public Img gray() { + final ColorConvertOp op = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), null); + this.targetImage = op.filter(ImgUtil.toBufferedImage(getValidSrcImg()), null); + return this; + } + + /** + * 彩色转为黑白二值化图片 + * + * @return this + */ + public Img binary() { + this.targetImage = ImgUtil.copyImage(getValidSrcImg(), BufferedImage.TYPE_BYTE_BINARY); + return this; + } + + /** + * 给图片添加文字水印
+ * 此方法并不关闭流 + * + * @param pressText 水印文字 + * @param color 水印的字体颜色 + * @param font {@link Font} 字体相关信息 + * @param x 修正值。 默认在中间,偏移量相对于中间偏移 + * @param y 修正值。 默认在中间,偏移量相对于中间偏移 + * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字 + * @return 处理后的图像 + */ + public Img pressText(String pressText, Color color, Font font, int x, int y, float alpha) { + final BufferedImage targetImage = ImgUtil.toBufferedImage(getValidSrcImg()); + final Graphics2D g = targetImage.createGraphics(); + + if (null == font) { + // 默认字体 + font = new Font("Courier", Font.PLAIN, (int) (targetImage.getHeight() * 0.75)); + } + + // 抗锯齿 + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setColor(color); + g.setFont(font); + // 透明度 + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha)); + // 在指定坐标绘制水印文字 + final FontMetrics metrics = g.getFontMetrics(font); + final int textLength = metrics.stringWidth(pressText); + final int textHeight = metrics.getAscent() - metrics.getLeading() - metrics.getDescent(); + g.drawString(pressText, Math.abs(targetImage.getWidth() - textLength) / 2 + x, Math.abs(targetImage.getHeight() + textHeight) / 2 + y); + g.dispose(); + this.targetImage = targetImage; + + return this; + } + + /** + * 给图片添加图片水印 + * + * @param pressImg 水印图片,可以使用{@link ImageIO#read(File)}方法读取文件 + * @param x 修正值。 默认在中间,偏移量相对于中间偏移 + * @param y 修正值。 默认在中间,偏移量相对于中间偏移 + * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字 + * @return this + */ + public Img pressImage(Image pressImg, int x, int y, float alpha) { + final int pressImgWidth = pressImg.getWidth(null); + final int pressImgHeight = pressImg.getHeight(null); + + return pressImage(pressImg, new Rectangle(x, y, pressImgWidth, pressImgHeight), alpha); + } + + /** + * 给图片添加图片水印 + * + * @param pressImg 水印图片,可以使用{@link ImageIO#read(File)}方法读取文件 + * @param rectangle 矩形对象,表示矩形区域的x,y,width,height,x,y从背景图片中心计算 + * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字 + * @return this + * @since 4.1.14 + */ + public Img pressImage(Image pressImg, Rectangle rectangle, float alpha) { + final Image targetImg = getValidSrcImg(); + + rectangle = fixRectangle(rectangle, targetImg.getWidth(null), targetImg.getHeight(null)); + this.targetImage = draw(ImgUtil.toBufferedImage(targetImg), pressImg, rectangle, alpha); + return this; + } + + /** + * 旋转图片为指定角度
+ * 来自:http://blog.51cto.com/cping1982/130066 + * + * @param degree 旋转角度 + * @return 旋转后的图片 + * @since 3.2.2 + */ + public Img rotate(int degree) { + final Image image = getValidSrcImg(); + int width = image.getWidth(null); + int height = image.getHeight(null); + final Rectangle rectangle = calcRotatedSize(width, height, degree); + final BufferedImage targetImg = new BufferedImage(rectangle.width, rectangle.height, getTypeInt()); + Graphics2D graphics2d = targetImg.createGraphics(); + // 抗锯齿 + graphics2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + // 从中心旋转 + graphics2d.translate((rectangle.width - width) / 2, (rectangle.height - height) / 2); + graphics2d.rotate(Math.toRadians(degree), width / 2, height / 2); + graphics2d.drawImage(image, 0, 0, null); + graphics2d.dispose(); + this.targetImage = targetImg; + return this; + } + + /** + * 水平翻转图像 + * + * @return this + */ + public Img flip() { + final Image image = getValidSrcImg(); + int width = image.getWidth(null); + int height = image.getHeight(null); + final BufferedImage targetImg = new BufferedImage(width, height, getTypeInt()); + Graphics2D graphics2d = targetImg.createGraphics(); + graphics2d.drawImage(image, 0, 0, width, height, width, 0, 0, height, null); + graphics2d.dispose(); + this.targetImage = targetImg; + return this; + } + + // ----------------------------------------------------------------------------------------------------------------- Write + /** + * 获取处理过的图片 + * + * @return 处理过的图片 + */ + public Image getImg() { + return this.targetImage; + } + + /** + * 写出图像 + * + * @param out 写出到的目标流 + * @return 是否成功写出,如果返回false表示未找到合适的Writer + * @throws IORuntimeException IO异常 + */ + public boolean write(OutputStream out) throws IORuntimeException { + return write(ImgUtil.getImageOutputStream(out)); + } + + /** + * 写出图像为PNG格式 + * + * @param targetImageStream 写出到的目标流 + * @return 是否成功写出,如果返回false表示未找到合适的Writer + * @throws IORuntimeException IO异常 + */ + public boolean write(ImageOutputStream targetImageStream) throws IORuntimeException { + Assert.notBlank(this.targetImageType, "Target image type is blank !"); + Assert.notNull(targetImageStream, "Target output stream is null !"); + + final Image targetImage = (null == this.targetImage) ? this.srcImage : this.targetImage; + Assert.notNull(targetImage, "Target image is null !"); + + return ImgUtil.write(targetImage, this.targetImageType, targetImageStream, this.quality); + } + + /** + * 写出图像为目标文件扩展名对应的格式 + * + * @param targetFile 目标文件 + * @return 是否成功写出,如果返回false表示未找到合适的Writer + * @throws IORuntimeException IO异常 + */ + public boolean write(File targetFile) throws IORuntimeException { + final String formatName = FileUtil.extName(targetFile); + if (StrUtil.isNotBlank(formatName)) { + this.targetImageType = formatName; + } + + if (targetFile.exists()) { + targetFile.delete(); + } + + ImageOutputStream out = null; + try { + out = ImgUtil.getImageOutputStream(targetFile); + return write(out); + } finally { + IoUtil.close(out); + } + } + + // ---------------------------------------------------------------------------------------------------------------- Private method start + /** + * 将图片绘制在背景上 + * + * @param backgroundImg 背景图片 + * @param img 要绘制的图片 + * @param rectangle 矩形对象,表示矩形区域的x,y,width,height,x,y从背景图片中心计算 + * @return 绘制后的背景 + */ + private static BufferedImage draw(BufferedImage backgroundImg, Image img, Rectangle rectangle, float alpha) { + final Graphics2D g = backgroundImg.createGraphics(); + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha)); + g.drawImage(img, rectangle.x, rectangle.y, rectangle.width, rectangle.height, null); // 绘制切割后的图 + g.dispose(); + return backgroundImg; + } + + /** + * 获取int类型的图片类型 + * + * @return 图片类型 + * @see BufferedImage#TYPE_INT_ARGB + * @see BufferedImage#TYPE_INT_RGB + */ + private int getTypeInt() { + switch (this.targetImageType) { + case ImgUtil.IMAGE_TYPE_PNG: + return BufferedImage.TYPE_INT_ARGB; + default: + return BufferedImage.TYPE_INT_RGB; + } + } + + /** + * 获取有效的源图片,首先检查上一次处理的结果图片,如无则使用用户传入的源图片 + * + * @return 有效的源图片 + */ + private Image getValidSrcImg() { + return ObjectUtil.defaultIfNull(this.targetImage, this.srcImage); + } + + /** + * 修正矩形框位置,如果{@link Img#setPositionFromCentre(boolean)} 设为{@code true},则坐标修正为基于图形中心,否则基于左上角 + * + * @param rectangle 矩形 + * @param baseWidth 参考宽 + * @param baseHeight 参考高 + * @return 修正后的{@link Rectangle} + * @since 4.1.15 + */ + private Rectangle fixRectangle(Rectangle rectangle, int baseWidth, int baseHeight) { + if (this.positionBaseCentre) { + // 修正图片位置从背景的中心计算 + rectangle.setLocation(// + rectangle.x + (int) (Math.abs(baseWidth - rectangle.width) / 2), // + rectangle.y + (int) (Math.abs(baseHeight - rectangle.height) / 2)// + ); + } + return rectangle; + } + + /** + * 计算旋转后的图片尺寸 + * + * @param width 宽度 + * @param height 高度 + * @param degree 旋转角度 + * @return 计算后目标尺寸 + * @since 4.1.20 + */ + private static Rectangle calcRotatedSize(int width, int height, int degree) { + if (degree >= 90) { + if (degree / 90 % 2 == 1) { + int temp = height; + height = width; + width = temp; + } + degree = degree % 90; + } + double r = Math.sqrt(height * height + width * width) / 2; + double len = 2 * Math.sin(Math.toRadians(degree) / 2) * r; + double angel_alpha = (Math.PI - Math.toRadians(degree)) / 2; + double angel_dalta_width = Math.atan((double) height / width); + double angel_dalta_height = Math.atan((double) width / height); + int len_dalta_width = (int) (len * Math.cos(Math.PI - angel_alpha - angel_dalta_width)); + int len_dalta_height = (int) (len * Math.cos(Math.PI - angel_alpha - angel_dalta_height)); + int des_width = width + len_dalta_width * 2; + int des_height = height + len_dalta_height * 2; + + return new Rectangle(des_width, des_height); + } + // ---------------------------------------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-core/src/main/java/cn/hutool/core/img/ImgUtil.java b/hutool-core/src/main/java/cn/hutool/core/img/ImgUtil.java new file mode 100644 index 000000000..dc35c0a97 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/img/ImgUtil.java @@ -0,0 +1,1844 @@ +package cn.hutool.core.img; + +import java.awt.Color; +import java.awt.Font; +import java.awt.FontFormatException; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Rectangle; +import java.awt.font.FontRenderContext; +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.awt.image.RenderedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.util.Iterator; +import java.util.List; +import java.util.Random; + +import javax.imageio.IIOImage; +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.ImageOutputStream; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.convert.Convert; +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.Resource; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 图片处理工具类:
+ * 功能:缩放图像、切割图像、旋转、图像类型转换、彩色转黑白、文字水印、图片水印等
+ * 参考:http://blog.csdn.net/zhangzhikaixinya/article/details/8459400 + * + * @author Looly + */ +public class ImgUtil { + + public static final String IMAGE_TYPE_GIF = "gif";// 图形交换格式 + public static final String IMAGE_TYPE_JPG = "jpg";// 联合照片专家组 + public static final String IMAGE_TYPE_JPEG = "jpeg";// 联合照片专家组 + public static final String IMAGE_TYPE_BMP = "bmp";// 英文Bitmap(位图)的简写,它是Windows操作系统中的标准图像文件格式 + public static final String IMAGE_TYPE_PNG = "png";// 可移植网络图形 + public static final String IMAGE_TYPE_PSD = "psd";// Photoshop的专用格式Photoshop + + // ---------------------------------------------------------------------------------------------------------------------- scale + /** + * 缩放图像(按比例缩放),目标文件的扩展名决定目标文件类型 + * + * @param srcImageFile 源图像文件 + * @param destImageFile 缩放后的图像文件,扩展名决定目标类型 + * @param scale 缩放比例。比例大于1时为放大,小于1大于0为缩小 + */ + public static void scale(File srcImageFile, File destImageFile, float scale) { + scale(read(srcImageFile), destImageFile, scale); + } + + /** + * 缩放图像(按比例缩放)
+ * 缩放后默认为jpeg格式,此方法并不关闭流 + * + * @param srcStream 源图像来源流 + * @param destStream 缩放后的图像写出到的流 + * @param scale 缩放比例。比例大于1时为放大,小于1大于0为缩小 + * @since 3.0.9 + */ + public static void scale(InputStream srcStream, OutputStream destStream, float scale) { + scale(read(srcStream), destStream, scale); + } + + /** + * 缩放图像(按比例缩放)
+ * 缩放后默认为jpeg格式,此方法并不关闭流 + * + * @param srcStream 源图像来源流 + * @param destStream 缩放后的图像写出到的流 + * @param scale 缩放比例。比例大于1时为放大,小于1大于0为缩小 + * @since 3.1.0 + */ + public static void scale(ImageInputStream srcStream, ImageOutputStream destStream, float scale) { + scale(read(srcStream), destStream, scale); + } + + /** + * 缩放图像(按比例缩放)
+ * 缩放后默认为jpeg格式,此方法并不关闭流 + * + * @param srcImg 源图像来源流 + * @param destFile 缩放后的图像写出到的流 + * @param scale 缩放比例。比例大于1时为放大,小于1大于0为缩小 + * @throws IORuntimeException IO异常 + * @since 3.2.2 + */ + public static void scale(Image srcImg, File destFile, float scale) throws IORuntimeException { + Img.from(srcImg).setTargetImageType(FileUtil.extName(destFile)).scale(scale).write(destFile); + } + + /** + * 缩放图像(按比例缩放)
+ * 缩放后默认为jpeg格式,此方法并不关闭流 + * + * @param srcImg 源图像来源流 + * @param out 缩放后的图像写出到的流 + * @param scale 缩放比例。比例大于1时为放大,小于1大于0为缩小 + * @throws IORuntimeException IO异常 + * @since 3.2.2 + */ + public static void scale(Image srcImg, OutputStream out, float scale) throws IORuntimeException { + scale(srcImg, getImageOutputStream(out), scale); + } + + /** + * 缩放图像(按比例缩放)
+ * 缩放后默认为jpeg格式,此方法并不关闭流 + * + * @param srcImg 源图像来源流 + * @param destImageStream 缩放后的图像写出到的流 + * @param scale 缩放比例。比例大于1时为放大,小于1大于0为缩小 + * @throws IORuntimeException IO异常 + * @since 3.1.0 + */ + public static void scale(Image srcImg, ImageOutputStream destImageStream, float scale) throws IORuntimeException { + writeJpg(scale(srcImg, scale), destImageStream); + } + + /** + * 缩放图像(按比例缩放) + * + * @param srcImg 源图像来源流 + * @param scale 缩放比例。比例大于1时为放大,小于1大于0为缩小 + * @return {@link Image} + * @since 3.1.0 + */ + public static Image scale(Image srcImg, float scale) { + return Img.from(srcImg).scale(scale).getImg(); + } + + /** + * 缩放图像(按长宽缩放)
+ * 注意:目标长宽与原图不成比例会变形 + * + * @param srcImg 源图像来源流 + * @param width 目标宽度 + * @param height 目标高度 + * @return {@link Image} + * @since 3.1.0 + */ + public static Image scale(Image srcImg, int width, int height) { + return Img.from(srcImg).scale(width, height).getImg(); + } + + /** + * 缩放图像(按高度和宽度缩放)
+ * 缩放后默认为jpeg格式 + * + * @param srcImageFile 源图像文件地址 + * @param destImageFile 缩放后的图像地址 + * @param width 缩放后的宽度 + * @param height 缩放后的高度 + * @param fixedColor 比例不对时补充的颜色,不补充为null + * @throws IORuntimeException IO异常 + */ + public static void scale(File srcImageFile, File destImageFile, int width, int height, Color fixedColor) throws IORuntimeException { + write(scale(read(srcImageFile), width, height, fixedColor), destImageFile); + } + + /** + * 缩放图像(按高度和宽度缩放)
+ * 缩放后默认为jpeg格式,此方法并不关闭流 + * + * @param srcStream 源图像流 + * @param destStream 缩放后的图像目标流 + * @param width 缩放后的宽度 + * @param height 缩放后的高度 + * @param fixedColor 比例不对时补充的颜色,不补充为null + * @throws IORuntimeException IO异常 + */ + public static void scale(InputStream srcStream, OutputStream destStream, int width, int height, Color fixedColor) throws IORuntimeException { + scale(read(srcStream), getImageOutputStream(destStream), width, height, fixedColor); + } + + /** + * 缩放图像(按高度和宽度缩放)
+ * 缩放后默认为jpeg格式,此方法并不关闭流 + * + * @param srcStream 源图像流 + * @param destStream 缩放后的图像目标流 + * @param width 缩放后的宽度 + * @param height 缩放后的高度 + * @param fixedColor 比例不对时补充的颜色,不补充为null + * @throws IORuntimeException IO异常 + */ + public static void scale(ImageInputStream srcStream, ImageOutputStream destStream, int width, int height, Color fixedColor) throws IORuntimeException { + scale(read(srcStream), destStream, width, height, fixedColor); + } + + /** + * 缩放图像(按高度和宽度缩放)
+ * 缩放后默认为jpeg格式,此方法并不关闭流 + * + * @param srcImage 源图像 + * @param destImageStream 缩放后的图像目标流 + * @param width 缩放后的宽度 + * @param height 缩放后的高度 + * @param fixedColor 比例不对时补充的颜色,不补充为null + * @throws IORuntimeException IO异常 + */ + public static void scale(Image srcImage, ImageOutputStream destImageStream, int width, int height, Color fixedColor) throws IORuntimeException { + writeJpg(scale(srcImage, width, height, fixedColor), destImageStream); + } + + /** + * 缩放图像(按高度和宽度缩放)
+ * 缩放后默认为jpeg格式 + * + * @param srcImage 源图像 + * @param width 缩放后的宽度 + * @param height 缩放后的高度 + * @param fixedColor 比例不对时补充的颜色,不补充为null + * @return {@link Image} + */ + public static Image scale(Image srcImage, int width, int height, Color fixedColor) { + return Img.from(srcImage).scale(width, height, fixedColor).getImg(); + } + + // ---------------------------------------------------------------------------------------------------------------------- cut + /** + * 图像切割(按指定起点坐标和宽高切割) + * + * @param srcImgFile 源图像文件 + * @param destImgFile 切片后的图像文件 + * @param rectangle 矩形对象,表示矩形区域的x,y,width,height + * @since 3.1.0 + */ + public static void cut(File srcImgFile, File destImgFile, Rectangle rectangle) { + cut(read(srcImgFile), destImgFile, rectangle); + } + + /** + * 图像切割(按指定起点坐标和宽高切割),此方法并不关闭流 + * + * @param srcStream 源图像流 + * @param destStream 切片后的图像输出流 + * @param rectangle 矩形对象,表示矩形区域的x,y,width,height + * @since 3.1.0 + */ + public static void cut(InputStream srcStream, OutputStream destStream, Rectangle rectangle) { + cut(read(srcStream), destStream, rectangle); + } + + /** + * 图像切割(按指定起点坐标和宽高切割),此方法并不关闭流 + * + * @param srcStream 源图像流 + * @param destStream 切片后的图像输出流 + * @param rectangle 矩形对象,表示矩形区域的x,y,width,height + * @since 3.1.0 + */ + public static void cut(ImageInputStream srcStream, ImageOutputStream destStream, Rectangle rectangle) { + cut(read(srcStream), destStream, rectangle); + } + + /** + * 图像切割(按指定起点坐标和宽高切割),此方法并不关闭流 + * + * @param srcImage 源图像 + * @param destFile 输出的文件 + * @param rectangle 矩形对象,表示矩形区域的x,y,width,height + * @since 3.2.2 + * @throws IORuntimeException IO异常 + */ + public static void cut(Image srcImage, File destFile, Rectangle rectangle) throws IORuntimeException { + write(cut(srcImage, rectangle), destFile); + } + + /** + * 图像切割(按指定起点坐标和宽高切割),此方法并不关闭流 + * + * @param srcImage 源图像 + * @param out 切片后的图像输出流 + * @param rectangle 矩形对象,表示矩形区域的x,y,width,height + * @since 3.1.0 + * @throws IORuntimeException IO异常 + */ + public static void cut(Image srcImage, OutputStream out, Rectangle rectangle) throws IORuntimeException { + cut(srcImage, getImageOutputStream(out), rectangle); + } + + /** + * 图像切割(按指定起点坐标和宽高切割),此方法并不关闭流 + * + * @param srcImage 源图像 + * @param destImageStream 切片后的图像输出流 + * @param rectangle 矩形对象,表示矩形区域的x,y,width,height + * @since 3.1.0 + * @throws IORuntimeException IO异常 + */ + public static void cut(Image srcImage, ImageOutputStream destImageStream, Rectangle rectangle) throws IORuntimeException { + writeJpg(cut(srcImage, rectangle), destImageStream); + } + + /** + * 图像切割(按指定起点坐标和宽高切割) + * + * @param srcImage 源图像 + * @param rectangle 矩形对象,表示矩形区域的x,y,width,height + * @return {@link BufferedImage} + * @since 3.1.0 + */ + public static Image cut(Image srcImage, Rectangle rectangle) { + return Img.from(srcImage).setPositionBaseCentre(false).cut(rectangle).getImg(); + } + + /** + * 图像切割(按指定起点坐标和宽高切割),填充满整个图片(直径取长宽最小值) + * + * @param srcImage 源图像 + * @param x 原图的x坐标起始位置 + * @param y 原图的y坐标起始位置 + * @return {@link Image} + * @since 4.1.15 + */ + public static Image cut(Image srcImage, int x, int y) { + return cut(srcImage, x, y, -1); + } + + /** + * 图像切割(按指定起点坐标和宽高切割) + * + * @param srcImage 源图像 + * @param x 原图的x坐标起始位置 + * @param y 原图的y坐标起始位置 + * @param radius 半径,小于0表示填充满整个图片(直径取长宽最小值) + * @return {@link Image} + * @since 4.1.15 + */ + public static Image cut(Image srcImage, int x, int y, int radius) { + return Img.from(srcImage).cut(x, y, radius).getImg(); + } + + /** + * 图像切片(指定切片的宽度和高度) + * + * @param srcImageFile 源图像 + * @param descDir 切片目标文件夹 + * @param destWidth 目标切片宽度。默认200 + * @param destHeight 目标切片高度。默认150 + */ + public static void slice(File srcImageFile, File descDir, int destWidth, int destHeight) { + slice(read(srcImageFile), descDir, destWidth, destHeight); + } + + /** + * 图像切片(指定切片的宽度和高度) + * + * @param srcImage 源图像 + * @param descDir 切片目标文件夹 + * @param destWidth 目标切片宽度。默认200 + * @param destHeight 目标切片高度。默认150 + */ + public static void slice(Image srcImage, File descDir, int destWidth, int destHeight) { + if (destWidth <= 0) { + destWidth = 200; // 切片宽度 + } + if (destHeight <= 0) { + destHeight = 150; // 切片高度 + } + int srcWidth = srcImage.getWidth(null); // 源图宽度 + int srcHeight = srcImage.getHeight(null); // 源图高度 + + try { + if (srcWidth > destWidth && srcHeight > destHeight) { + int cols = 0; // 切片横向数量 + int rows = 0; // 切片纵向数量 + // 计算切片的横向和纵向数量 + if (srcWidth % destWidth == 0) { + cols = srcWidth / destWidth; + } else { + cols = (int) Math.floor(srcWidth / destWidth) + 1; + } + if (srcHeight % destHeight == 0) { + rows = srcHeight / destHeight; + } else { + rows = (int) Math.floor(srcHeight / destHeight) + 1; + } + // 循环建立切片 + Image tag; + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + // 四个参数分别为图像起点坐标和宽高 + // 即: CropImageFilter(int x,int y,int width,int height) + tag = cut(srcImage, new Rectangle(j * destWidth, i * destHeight, destWidth, destHeight)); + // 输出为文件 + ImageIO.write(toRenderedImage(tag), IMAGE_TYPE_JPEG, new File(descDir, "_r" + i + "_c" + j + ".jpg")); + } + } + } + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 图像切割(指定切片的行数和列数) + * + * @param srcImageFile 源图像文件 + * @param destDir 切片目标文件夹 + * @param rows 目标切片行数。默认2,必须是范围 [1, 20] 之内 + * @param cols 目标切片列数。默认2,必须是范围 [1, 20] 之内 + */ + public static void sliceByRowsAndCols(File srcImageFile, File destDir, int rows, int cols) { + try { + sliceByRowsAndCols(ImageIO.read(srcImageFile), destDir, rows, cols); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 图像切割(指定切片的行数和列数) + * + * @param srcImage 源图像 + * @param destDir 切片目标文件夹 + * @param rows 目标切片行数。默认2,必须是范围 [1, 20] 之内 + * @param cols 目标切片列数。默认2,必须是范围 [1, 20] 之内 + */ + public static void sliceByRowsAndCols(Image srcImage, File destDir, int rows, int cols) { + if (false == destDir.exists()) { + FileUtil.mkdir(destDir); + } else if (false == destDir.isDirectory()) { + throw new IllegalArgumentException("Destination Dir must be a Directory !"); + } + + try { + if (rows <= 0 || rows > 20) { + rows = 2; // 切片行数 + } + if (cols <= 0 || cols > 20) { + cols = 2; // 切片列数 + } + // 读取源图像 + final Image bi = toBufferedImage(srcImage); + int srcWidth = bi.getWidth(null); // 源图宽度 + int srcHeight = bi.getHeight(null); // 源图高度 + + int destWidth = NumberUtil.partValue(srcWidth, cols); // 每张切片的宽度 + int destHeight = NumberUtil.partValue(srcHeight, rows); // 每张切片的高度 + + // 循环建立切片 + Image tag; + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + tag = cut(bi, new Rectangle(j * destWidth, i * destHeight, destWidth, destHeight)); + // 输出为文件 + ImageIO.write(toRenderedImage(tag), IMAGE_TYPE_JPEG, new File(destDir, "_r" + i + "_c" + j + ".jpg")); + } + } + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + // ---------------------------------------------------------------------------------------------------------------------- convert + /** + * 图像类型转换:GIF=》JPG、GIF=》PNG、PNG=》JPG、PNG=》GIF(X)、BMP=》PNG + * + * @param srcImageFile 源图像文件 + * @param destImageFile 目标图像文件 + */ + public static void convert(File srcImageFile, File destImageFile) { + Assert.notNull(srcImageFile); + Assert.notNull(destImageFile); + Assert.isFalse(srcImageFile.equals(destImageFile), "Src file is equals to dest file!"); + + final String srcExtName = FileUtil.extName(srcImageFile); + final String destExtName = FileUtil.extName(destImageFile); + if (StrUtil.equalsIgnoreCase(srcExtName, destExtName)) { + // 扩展名相同直接复制文件 + FileUtil.copy(srcImageFile, destImageFile, true); + } + + ImageOutputStream imageOutputStream = null; + try { + imageOutputStream = getImageOutputStream(destImageFile); + convert(read(srcImageFile), destExtName, imageOutputStream, StrUtil.equalsIgnoreCase(IMAGE_TYPE_PNG, srcExtName)); + } finally { + IoUtil.close(imageOutputStream); + } + } + + /** + * 图像类型转换:GIF=》JPG、GIF=》PNG、PNG=》JPG、PNG=》GIF(X)、BMP=》PNG
+ * 此方法并不关闭流 + * + * @param srcStream 源图像流 + * @param formatName 包含格式非正式名称的 String:如JPG、JPEG、GIF等 + * @param destStream 目标图像输出流 + * @since 3.0.9 + */ + public static void convert(InputStream srcStream, String formatName, OutputStream destStream) { + write(read(srcStream), formatName, getImageOutputStream(destStream)); + } + + /** + * 图像类型转换:GIF=》JPG、GIF=》PNG、PNG=》JPG、PNG=》GIF(X)、BMP=》PNG
+ * 此方法并不关闭流 + * + * @param srcStream 源图像流 + * @param formatName 包含格式非正式名称的 String:如JPG、JPEG、GIF等 + * @param destStream 目标图像输出流 + * @since 3.0.9 + * @deprecated 请使用{@link #write(Image, String, ImageOutputStream)} + */ + @Deprecated + public static void convert(ImageInputStream srcStream, String formatName, ImageOutputStream destStream) { + write(read(srcStream), formatName, destStream); + } + + /** + * 图像类型转换:GIF=》JPG、GIF=》PNG、PNG=》JPG、PNG=》GIF(X)、BMP=》PNG
+ * 此方法并不关闭流 + * + * @param srcImage 源图像流 + * @param formatName 包含格式非正式名称的 String:如JPG、JPEG、GIF等 + * @param destImageStream 目标图像输出流 + * @since 3.0.9 + * @deprecated 请使用{@link #write(Image, String, ImageOutputStream)} + */ + @Deprecated + public static void convert(Image srcImage, String formatName, ImageOutputStream destImageStream) { + convert(srcImage, formatName, destImageStream, false); + } + + /** + * 图像类型转换:GIF=》JPG、GIF=》PNG、PNG=》JPG、PNG=》GIF(X)、BMP=》PNG
+ * 此方法并不关闭流 + * + * @param srcImage 源图像流 + * @param formatName 包含格式非正式名称的 String:如JPG、JPEG、GIF等 + * @param destImageStream 目标图像输出流 + * @param isSrcPng 源图片是否为PNG格式 + * @since 4.1.14 + */ + public static void convert(Image srcImage, String formatName, ImageOutputStream destImageStream, boolean isSrcPng) { + try { + ImageIO.write(isSrcPng ? copyImage(srcImage, BufferedImage.TYPE_INT_RGB) : toBufferedImage(srcImage), formatName, destImageStream); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + // ---------------------------------------------------------------------------------------------------------------------- grey + /** + * 彩色转为黑白 + * + * @param srcImageFile 源图像地址 + * @param destImageFile 目标图像地址 + */ + public static void gray(File srcImageFile, File destImageFile) { + gray(read(srcImageFile), destImageFile); + } + + /** + * 彩色转为黑白
+ * 此方法并不关闭流 + * + * @param srcStream 源图像流 + * @param destStream 目标图像流 + * @since 3.0.9 + */ + public static void gray(InputStream srcStream, OutputStream destStream) { + gray(read(srcStream), getImageOutputStream(destStream)); + } + + /** + * 彩色转为黑白
+ * 此方法并不关闭流 + * + * @param srcStream 源图像流 + * @param destStream 目标图像流 + * @since 3.0.9 + */ + public static void gray(ImageInputStream srcStream, ImageOutputStream destStream) { + gray(read(srcStream), destStream); + } + + /** + * 彩色转为黑白 + * + * @param srcImage 源图像流 + * @param outFile 目标文件 + * @since 3.2.2 + */ + public static void gray(Image srcImage, File outFile) { + write(gray(srcImage), outFile); + } + + /** + * 彩色转为黑白
+ * 此方法并不关闭流 + * + * @param srcImage 源图像流 + * @param out 目标图像流 + * @since 3.2.2 + */ + public static void gray(Image srcImage, OutputStream out) { + gray(srcImage, getImageOutputStream(out)); + } + + /** + * 彩色转为黑白
+ * 此方法并不关闭流 + * + * @param srcImage 源图像流 + * @param destImageStream 目标图像流 + * @since 3.0.9 + * @throws IORuntimeException IO异常 + */ + public static void gray(Image srcImage, ImageOutputStream destImageStream) throws IORuntimeException { + writeJpg(gray(srcImage), destImageStream); + } + + /** + * 彩色转为黑白 + * + * @param srcImage 源图像流 + * @return {@link Image}灰度后的图片 + * @since 3.1.0 + */ + public static Image gray(Image srcImage) { + return Img.from(srcImage).gray().getImg(); + } + + // ---------------------------------------------------------------------------------------------------------------------- binary + /** + * 彩色转为黑白二值化图片,根据目标文件扩展名确定转换后的格式 + * + * @param srcImageFile 源图像地址 + * @param destImageFile 目标图像地址 + */ + public static void binary(File srcImageFile, File destImageFile) { + binary(read(srcImageFile), destImageFile); + } + + /** + * 彩色转为黑白二值化图片
+ * 此方法并不关闭流 + * + * @param srcStream 源图像流 + * @param destStream 目标图像流 + * @param imageType 图片格式(扩展名) + * @since 4.0.5 + */ + public static void binary(InputStream srcStream, OutputStream destStream, String imageType) { + binary(read(srcStream), getImageOutputStream(destStream), imageType); + } + + /** + * 彩色转为黑白黑白二值化图片
+ * 此方法并不关闭流 + * + * @param srcStream 源图像流 + * @param destStream 目标图像流 + * @param imageType 图片格式(扩展名) + * @since 4.0.5 + */ + public static void binary(ImageInputStream srcStream, ImageOutputStream destStream, String imageType) { + binary(read(srcStream), destStream, imageType); + } + + /** + * 彩色转为黑白二值化图片,根据目标文件扩展名确定转换后的格式 + * + * @param srcImage 源图像流 + * @param outFile 目标文件 + * @since 4.0.5 + */ + public static void binary(Image srcImage, File outFile) { + write(binary(srcImage), outFile); + } + + /** + * 彩色转为黑白二值化图片
+ * 此方法并不关闭流,输出JPG格式 + * + * @param srcImage 源图像流 + * @param out 目标图像流 + * @param imageType 图片格式(扩展名) + * @since 4.0.5 + */ + public static void binary(Image srcImage, OutputStream out, String imageType) { + binary(srcImage, getImageOutputStream(out), imageType); + } + + /** + * 彩色转为黑白二值化图片
+ * 此方法并不关闭流,输出JPG格式 + * + * @param srcImage 源图像流 + * @param destImageStream 目标图像流 + * @param imageType 图片格式(扩展名) + * @since 4.0.5 + * @throws IORuntimeException IO异常 + */ + public static void binary(Image srcImage, ImageOutputStream destImageStream, String imageType) throws IORuntimeException { + write(binary(srcImage), imageType, destImageStream); + } + + /** + * 彩色转为黑白二值化图片 + * + * @param srcImage 源图像流 + * @return {@link Image}二值化后的图片 + * @since 4.0.5 + */ + public static Image binary(Image srcImage) { + return Img.from(srcImage).binary().getImg(); + } + + // ---------------------------------------------------------------------------------------------------------------------- press + /** + * 给图片添加文字水印 + * + * @param imageFile 源图像文件 + * @param destFile 目标图像文件 + * @param pressText 水印文字 + * @param color 水印的字体颜色 + * @param font {@link Font} 字体相关信息,如果默认则为{@code null} + * @param x 修正值。 默认在中间,偏移量相对于中间偏移 + * @param y 修正值。 默认在中间,偏移量相对于中间偏移 + * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字 + */ + public static void pressText(File imageFile, File destFile, String pressText, Color color, Font font, int x, int y, float alpha) { + pressText(read(imageFile), destFile, pressText, color, font, x, y, alpha); + } + + /** + * 给图片添加文字水印
+ * 此方法并不关闭流 + * + * @param srcStream 源图像流 + * @param destStream 目标图像流 + * @param pressText 水印文字 + * @param color 水印的字体颜色 + * @param font {@link Font} 字体相关信息,如果默认则为{@code null} + * @param x 修正值。 默认在中间,偏移量相对于中间偏移 + * @param y 修正值。 默认在中间,偏移量相对于中间偏移 + * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字 + */ + public static void pressText(InputStream srcStream, OutputStream destStream, String pressText, Color color, Font font, int x, int y, float alpha) { + pressText(read(srcStream), getImageOutputStream(destStream), pressText, color, font, x, y, alpha); + } + + /** + * 给图片添加文字水印
+ * 此方法并不关闭流 + * + * @param srcStream 源图像流 + * @param destStream 目标图像流 + * @param pressText 水印文字 + * @param color 水印的字体颜色 + * @param font {@link Font} 字体相关信息,如果默认则为{@code null} + * @param x 修正值。 默认在中间,偏移量相对于中间偏移 + * @param y 修正值。 默认在中间,偏移量相对于中间偏移 + * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字 + */ + public static void pressText(ImageInputStream srcStream, ImageOutputStream destStream, String pressText, Color color, Font font, int x, int y, float alpha) { + pressText(read(srcStream), destStream, pressText, color, font, x, y, alpha); + } + + /** + * 给图片添加文字水印
+ * 此方法并不关闭流 + * + * @param srcImage 源图像 + * @param destFile 目标流 + * @param pressText 水印文字 + * @param color 水印的字体颜色 + * @param font {@link Font} 字体相关信息,如果默认则为{@code null} + * @param x 修正值。 默认在中间,偏移量相对于中间偏移 + * @param y 修正值。 默认在中间,偏移量相对于中间偏移 + * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字 + * @throws IORuntimeException IO异常 + * @since 3.2.2 + */ + public static void pressText(Image srcImage, File destFile, String pressText, Color color, Font font, int x, int y, float alpha) throws IORuntimeException { + write(pressText(srcImage, pressText, color, font, x, y, alpha), destFile); + } + + /** + * 给图片添加文字水印
+ * 此方法并不关闭流 + * + * @param srcImage 源图像 + * @param to 目标流 + * @param pressText 水印文字 + * @param color 水印的字体颜色 + * @param font {@link Font} 字体相关信息,如果默认则为{@code null} + * @param x 修正值。 默认在中间,偏移量相对于中间偏移 + * @param y 修正值。 默认在中间,偏移量相对于中间偏移 + * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字 + * @throws IORuntimeException IO异常 + * @since 3.2.2 + */ + public static void pressText(Image srcImage, OutputStream to, String pressText, Color color, Font font, int x, int y, float alpha) throws IORuntimeException { + pressText(srcImage, getImageOutputStream(to), pressText, color, font, x, y, alpha); + } + + /** + * 给图片添加文字水印
+ * 此方法并不关闭流 + * + * @param srcImage 源图像 + * @param destImageStream 目标图像流 + * @param pressText 水印文字 + * @param color 水印的字体颜色 + * @param font {@link Font} 字体相关信息,如果默认则为{@code null} + * @param x 修正值。 默认在中间,偏移量相对于中间偏移 + * @param y 修正值。 默认在中间,偏移量相对于中间偏移 + * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字 + * @throws IORuntimeException IO异常 + */ + public static void pressText(Image srcImage, ImageOutputStream destImageStream, String pressText, Color color, Font font, int x, int y, float alpha) throws IORuntimeException { + writeJpg(pressText(srcImage, pressText, color, font, x, y, alpha), destImageStream); + } + + /** + * 给图片添加文字水印
+ * 此方法并不关闭流 + * + * @param srcImage 源图像 + * @param pressText 水印文字 + * @param color 水印的字体颜色 + * @param font {@link Font} 字体相关信息,如果默认则为{@code null} + * @param x 修正值。 默认在中间,偏移量相对于中间偏移 + * @param y 修正值。 默认在中间,偏移量相对于中间偏移 + * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字 + * @return 处理后的图像 + * @since 3.2.2 + */ + public static Image pressText(Image srcImage, String pressText, Color color, Font font, int x, int y, float alpha) { + return Img.from(srcImage).pressText(pressText, color, font, x, y, alpha).getImg(); + } + + /** + * 给图片添加图片水印 + * + * @param srcImageFile 源图像文件 + * @param destImageFile 目标图像文件 + * @param pressImg 水印图片 + * @param x 修正值。 默认在中间,偏移量相对于中间偏移 + * @param y 修正值。 默认在中间,偏移量相对于中间偏移 + * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字 + */ + public static void pressImage(File srcImageFile, File destImageFile, Image pressImg, int x, int y, float alpha) { + pressImage(read(srcImageFile), destImageFile, pressImg, x, y, alpha); + } + + /** + * 给图片添加图片水印
+ * 此方法并不关闭流 + * + * @param srcStream 源图像流 + * @param destStream 目标图像流 + * @param pressImg 水印图片,可以使用{@link ImageIO#read(File)}方法读取文件 + * @param x 修正值。 默认在中间,偏移量相对于中间偏移 + * @param y 修正值。 默认在中间,偏移量相对于中间偏移 + * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字 + */ + public static void pressImage(InputStream srcStream, OutputStream destStream, Image pressImg, int x, int y, float alpha) { + pressImage(read(srcStream), getImageOutputStream(destStream), pressImg, x, y, alpha); + } + + /** + * 给图片添加图片水印
+ * 此方法并不关闭流 + * + * @param srcStream 源图像流 + * @param destStream 目标图像流 + * @param pressImg 水印图片,可以使用{@link ImageIO#read(File)}方法读取文件 + * @param x 修正值。 默认在中间,偏移量相对于中间偏移 + * @param y 修正值。 默认在中间,偏移量相对于中间偏移 + * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字 + * @throws IORuntimeException IO异常 + */ + public static void pressImage(ImageInputStream srcStream, ImageOutputStream destStream, Image pressImg, int x, int y, float alpha) throws IORuntimeException { + pressImage(read(srcStream), destStream, pressImg, x, y, alpha); + } + + /** + * 给图片添加图片水印
+ * 此方法并不关闭流 + * + * @param srcImage 源图像流 + * @param outFile 写出文件 + * @param pressImg 水印图片,可以使用{@link ImageIO#read(File)}方法读取文件 + * @param x 修正值。 默认在中间,偏移量相对于中间偏移 + * @param y 修正值。 默认在中间,偏移量相对于中间偏移 + * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字 + * @throws IORuntimeException IO异常 + * @since 3.2.2 + */ + public static void pressImage(Image srcImage, File outFile, Image pressImg, int x, int y, float alpha) throws IORuntimeException { + write(pressImage(srcImage, pressImg, x, y, alpha), outFile); + } + + /** + * 给图片添加图片水印
+ * 此方法并不关闭流 + * + * @param srcImage 源图像流 + * @param out 目标图像流 + * @param pressImg 水印图片,可以使用{@link ImageIO#read(File)}方法读取文件 + * @param x 修正值。 默认在中间,偏移量相对于中间偏移 + * @param y 修正值。 默认在中间,偏移量相对于中间偏移 + * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字 + * @throws IORuntimeException IO异常 + * @since 3.2.2 + */ + public static void pressImage(Image srcImage, OutputStream out, Image pressImg, int x, int y, float alpha) throws IORuntimeException { + pressImage(srcImage, getImageOutputStream(out), pressImg, x, y, alpha); + } + + /** + * 给图片添加图片水印
+ * 此方法并不关闭流 + * + * @param srcImage 源图像流 + * @param destImageStream 目标图像流 + * @param pressImg 水印图片,可以使用{@link ImageIO#read(File)}方法读取文件 + * @param x 修正值。 默认在中间,偏移量相对于中间偏移 + * @param y 修正值。 默认在中间,偏移量相对于中间偏移 + * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字 + * @throws IORuntimeException IO异常 + */ + public static void pressImage(Image srcImage, ImageOutputStream destImageStream, Image pressImg, int x, int y, float alpha) throws IORuntimeException { + writeJpg(pressImage(srcImage, pressImg, x, y, alpha), destImageStream); + } + + /** + * 给图片添加图片水印
+ * 此方法并不关闭流 + * + * @param srcImage 源图像流 + * @param pressImg 水印图片,可以使用{@link ImageIO#read(File)}方法读取文件 + * @param x 修正值。 默认在中间,偏移量相对于中间偏移 + * @param y 修正值。 默认在中间,偏移量相对于中间偏移 + * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字 + * @return 结果图片 + */ + public static Image pressImage(Image srcImage, Image pressImg, int x, int y, float alpha) { + return Img.from(srcImage).pressImage(pressImg, x, y, alpha).getImg(); + } + + /** + * 给图片添加图片水印
+ * 此方法并不关闭流 + * + * @param srcImage 源图像流 + * @param pressImg 水印图片,可以使用{@link ImageIO#read(File)}方法读取文件 + * @param rectangle 矩形对象,表示矩形区域的x,y,width,height,x,y从背景图片中心计算 + * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字 + * @return 结果图片 + * @since 4.1.14 + */ + public static Image pressImage(Image srcImage, Image pressImg, Rectangle rectangle, float alpha) { + return Img.from(srcImage).pressImage(pressImg, rectangle, alpha).getImg(); + } + + // ---------------------------------------------------------------------------------------------------------------------- rotate + /** + * 旋转图片为指定角度
+ * 此方法不会关闭输出流 + * + * @param imageFile 被旋转图像文件 + * @param degree 旋转角度 + * @param outFile 输出文件 + * @since 3.2.2 + * @throws IORuntimeException IO异常 + */ + public static void rotate(File imageFile, int degree, File outFile) throws IORuntimeException { + rotate(read(imageFile), degree, outFile); + } + + /** + * 旋转图片为指定角度
+ * 此方法不会关闭输出流 + * + * @param image 目标图像 + * @param degree 旋转角度 + * @param outFile 输出文件 + * @since 3.2.2 + * @throws IORuntimeException IO异常 + */ + public static void rotate(Image image, int degree, File outFile) throws IORuntimeException { + write(rotate(image, degree), outFile); + } + + /** + * 旋转图片为指定角度
+ * 此方法不会关闭输出流 + * + * @param image 目标图像 + * @param degree 旋转角度 + * @param out 输出流 + * @since 3.2.2 + * @throws IORuntimeException IO异常 + */ + public static void rotate(Image image, int degree, OutputStream out) throws IORuntimeException { + writeJpg(rotate(image, degree), getImageOutputStream(out)); + } + + /** + * 旋转图片为指定角度
+ * 此方法不会关闭输出流,输出格式为JPG + * + * @param image 目标图像 + * @param degree 旋转角度 + * @param out 输出图像流 + * @since 3.2.2 + * @throws IORuntimeException IO异常 + */ + public static void rotate(Image image, int degree, ImageOutputStream out) throws IORuntimeException { + writeJpg(rotate(image, degree), out); + } + + /** + * 旋转图片为指定角度
+ * 来自:http://blog.51cto.com/cping1982/130066 + * + * @param image 目标图像 + * @param degree 旋转角度 + * @return 旋转后的图片 + * @since 3.2.2 + */ + public static Image rotate(Image image, int degree) { + return Img.from(image).rotate(degree).getImg(); + } + + // ---------------------------------------------------------------------------------------------------------------------- flip + /** + * 水平翻转图像 + * + * @param imageFile 图像文件 + * @param outFile 输出文件 + * @throws IORuntimeException IO异常 + * @since 3.2.2 + */ + public static void flip(File imageFile, File outFile) throws IORuntimeException { + flip(read(imageFile), outFile); + } + + /** + * 水平翻转图像 + * + * @param image 图像 + * @param outFile 输出文件 + * @throws IORuntimeException IO异常 + * @since 3.2.2 + */ + public static void flip(Image image, File outFile) throws IORuntimeException { + write(flip(image), outFile); + } + + /** + * 水平翻转图像 + * + * @param image 图像 + * @param out 输出 + * @throws IORuntimeException IO异常 + * @since 3.2.2 + */ + public static void flip(Image image, OutputStream out) throws IORuntimeException { + flip(image, getImageOutputStream(out)); + } + + /** + * 水平翻转图像,写出格式为JPG + * + * @param image 图像 + * @param out 输出 + * @throws IORuntimeException IO异常 + * @since 3.2.2 + */ + public static void flip(Image image, ImageOutputStream out) throws IORuntimeException { + writeJpg(flip(image), out); + } + + /** + * 水平翻转图像 + * + * @param image 图像 + * @return 翻转后的图片 + * @since 3.2.2 + */ + public static Image flip(Image image) { + return Img.from(image).flip().getImg(); + } + + // ---------------------------------------------------------------------------------------------------------------------- compress + /** + * 压缩图像,输出图像只支持jpg文件 + * + * @param imageFile 图像文件 + * @param outFile 输出文件,只支持jpg文件 + * @throws IORuntimeException IO异常 + * @since 4.3.2 + */ + public static void compress(File imageFile, File outFile, float quality) throws IORuntimeException { + Img.from(imageFile).setQuality(quality).write(outFile); + } + + // ---------------------------------------------------------------------------------------------------------------------- other + /** + * {@link Image} 转 {@link RenderedImage}
+ * 首先尝试强转,否则新建一个{@link BufferedImage}后重新绘制 + * + * @param img {@link Image} + * @return {@link BufferedImage} + * @since 4.3.2 + */ + public static RenderedImage toRenderedImage(Image img) { + if (img instanceof RenderedImage) { + return (RenderedImage) img; + } + + return copyImage(img, BufferedImage.TYPE_INT_RGB); + } + + /** + * {@link Image} 转 {@link BufferedImage}
+ * 首先尝试强转,否则新建一个{@link BufferedImage}后重新绘制 + * + * @param img {@link Image} + * @return {@link BufferedImage} + */ + public static BufferedImage toBufferedImage(Image img) { + if (img instanceof BufferedImage) { + return (BufferedImage) img; + } + + return copyImage(img, BufferedImage.TYPE_INT_RGB); + } + + /** + * {@link Image} 转 {@link BufferedImage}
+ * 如果源图片的RGB模式与目标模式一致,则直接转换,否则重新绘制 + * + * @param image {@link Image} + * @param imageType 目标图片类型 + * @return {@link BufferedImage} + * @since 4.3.2 + */ + public static BufferedImage toBufferedImage(Image image, String imageType) { + BufferedImage bufferedImage; + if (false == imageType.equalsIgnoreCase(IMAGE_TYPE_PNG)) { + // 当目标为非PNG类图片时,源图片统一转换为RGB格式 + if (image instanceof BufferedImage) { + bufferedImage = (BufferedImage) image; + if (BufferedImage.TYPE_INT_RGB != bufferedImage.getType()) { + bufferedImage = copyImage(image, BufferedImage.TYPE_INT_RGB); + } + } else { + bufferedImage = copyImage(image, BufferedImage.TYPE_INT_RGB); + } + } else { + bufferedImage = toBufferedImage(image); + } + return bufferedImage; + } + + /** + * 将已有Image复制新的一份出来 + * + * @param img {@link Image} + * @param imageType 目标图片类型,{@link BufferedImage}中的常量,例如黑白等 + * @return {@link BufferedImage} + * @see BufferedImage#TYPE_INT_RGB + * @see BufferedImage#TYPE_INT_ARGB + * @see BufferedImage#TYPE_INT_ARGB_PRE + * @see BufferedImage#TYPE_INT_BGR + * @see BufferedImage#TYPE_3BYTE_BGR + * @see BufferedImage#TYPE_4BYTE_ABGR + * @see BufferedImage#TYPE_4BYTE_ABGR_PRE + * @see BufferedImage#TYPE_BYTE_GRAY + * @see BufferedImage#TYPE_USHORT_GRAY + * @see BufferedImage#TYPE_BYTE_BINARY + * @see BufferedImage#TYPE_BYTE_INDEXED + * @see BufferedImage#TYPE_USHORT_565_RGB + * @see BufferedImage#TYPE_USHORT_555_RGB + */ + public static BufferedImage copyImage(Image img, int imageType) { + return copyImage(img, imageType, null); + } + + /** + * 将已有Image复制新的一份出来 + * + * @param img {@link Image} + * @param imageType 目标图片类型,{@link BufferedImage}中的常量,例如黑白等 + * @param backgroundColor 背景色,{@code null} 表示默认背景色(黑色或者透明) + * @return {@link BufferedImage} + * @see BufferedImage#TYPE_INT_RGB + * @see BufferedImage#TYPE_INT_ARGB + * @see BufferedImage#TYPE_INT_ARGB_PRE + * @see BufferedImage#TYPE_INT_BGR + * @see BufferedImage#TYPE_3BYTE_BGR + * @see BufferedImage#TYPE_4BYTE_ABGR + * @see BufferedImage#TYPE_4BYTE_ABGR_PRE + * @see BufferedImage#TYPE_BYTE_GRAY + * @see BufferedImage#TYPE_USHORT_GRAY + * @see BufferedImage#TYPE_BYTE_BINARY + * @see BufferedImage#TYPE_BYTE_INDEXED + * @see BufferedImage#TYPE_USHORT_565_RGB + * @see BufferedImage#TYPE_USHORT_555_RGB + * @since 4.5.17 + */ + public static BufferedImage copyImage(Image img, int imageType, Color backgroundColor) { + final BufferedImage bimage = new BufferedImage(img.getWidth(null), img.getHeight(null), imageType); + final Graphics2D bGr = GraphicsUtil.createGraphics(bimage, backgroundColor); + bGr.drawImage(img, 0, 0, null); + bGr.dispose(); + + return bimage; + } + + /** + * 将Base64编码的图像信息转为 {@link BufferedImage} + * + * @param base64 图像的Base64表示 + * @return {@link BufferedImage} + * @throws IORuntimeException IO异常 + */ + public static BufferedImage toImage(String base64) throws IORuntimeException { + return toImage(Base64.decode(base64)); + } + + /** + * 将的图像bytes转为 {@link BufferedImage} + * + * @param imageBytes 图像bytes + * @return {@link BufferedImage} + * @throws IORuntimeException IO异常 + */ + public static BufferedImage toImage(byte[] imageBytes) throws IORuntimeException { + return read(new ByteArrayInputStream(imageBytes)); + } + + /** + * 将图片对象转换为Base64形式 + * + * @param image 图片对象 + * @param imageType 图片类型 + * @return Base64的字符串表现形式 + * @since 4.1.8 + */ + public static String toBase64(Image image, String imageType) { + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + write(image, imageType, out); + return Base64.encode(out.toByteArray()); + } + + /** + * 根据文字创建PNG图片 + * + * @param str 文字 + * @param font 字体{@link Font} + * @param backgroundColor 背景颜色 + * @param fontColor 字体颜色 + * @param out 图片输出地 + * @throws IORuntimeException IO异常 + */ + public static void createImage(String str, Font font, Color backgroundColor, Color fontColor, ImageOutputStream out) throws IORuntimeException { + // 获取font的样式应用在str上的整个矩形 + Rectangle2D r = font.getStringBounds(str, new FontRenderContext(AffineTransform.getScaleInstance(1, 1), false, false)); + int unitHeight = (int) Math.floor(r.getHeight());// 获取单个字符的高度 + // 获取整个str用了font样式的宽度这里用四舍五入后+1保证宽度绝对能容纳这个字符串作为图片的宽度 + int width = (int) Math.round(r.getWidth()) + 1; + int height = unitHeight + 3;// 把单个字符的高度+3保证高度绝对能容纳字符串作为图片的高度 + // 创建图片 + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_BGR); + Graphics g = image.getGraphics(); + g.setColor(backgroundColor); + g.fillRect(0, 0, width, height);// 先用背景色填充整张图片,也就是背景 + g.setColor(fontColor); + g.setFont(font);// 设置画笔字体 + g.drawString(str, 0, font.getSize());// 画出字符串 + g.dispose(); + writePng(image, out); + } + + /** + * 根据文件创建字体
+ * 首先尝试创建{@link Font#TRUETYPE_FONT}字体,此类字体无效则创建{@link Font#TYPE1_FONT} + * + * @param fontFile 字体文件 + * @return {@link Font} + * @since 3.0.9 + */ + public static Font createFont(File fontFile) { + try { + return Font.createFont(Font.TRUETYPE_FONT, fontFile); + } catch (FontFormatException e) { + // True Type字体无效时使用Type1字体 + try { + return Font.createFont(Font.TYPE1_FONT, fontFile); + } catch (Exception e1) { + throw new UtilException(e); + } + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 根据文件创建字体
+ * 首先尝试创建{@link Font#TRUETYPE_FONT}字体,此类字体无效则创建{@link Font#TYPE1_FONT} + * + * @param fontStream 字体流 + * @return {@link Font} + * @since 3.0.9 + */ + public static Font createFont(InputStream fontStream) { + try { + return Font.createFont(Font.TRUETYPE_FONT, fontStream); + } catch (FontFormatException e) { + // True Type字体无效时使用Type1字体 + try { + return Font.createFont(Font.TYPE1_FONT, fontStream); + } catch (Exception e1) { + throw new UtilException(e1); + } + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 创建{@link Graphics2D} + * + * @param image {@link BufferedImage} + * @param color {@link Color}背景颜色以及当前画笔颜色 + * @return {@link Graphics2D} + * @since 3.2.3 + * @see GraphicsUtil#createGraphics(BufferedImage, Color) + */ + public static Graphics2D createGraphics(BufferedImage image, Color color) { + return GraphicsUtil.createGraphics(image, color); + } + + /** + * 写出图像为JPG格式 + * + * @param image {@link Image} + * @param destImageStream 写出到的目标流 + * @throws IORuntimeException IO异常 + */ + public static void writeJpg(Image image, ImageOutputStream destImageStream) throws IORuntimeException { + write(image, IMAGE_TYPE_JPG, destImageStream); + } + + /** + * 写出图像为PNG格式 + * + * @param image {@link Image} + * @param destImageStream 写出到的目标流 + * @throws IORuntimeException IO异常 + */ + public static void writePng(Image image, ImageOutputStream destImageStream) throws IORuntimeException { + write(image, IMAGE_TYPE_PNG, destImageStream); + } + + /** + * 写出图像为JPG格式 + * + * @param image {@link Image} + * @param out 写出到的目标流 + * @throws IORuntimeException IO异常 + * @since 4.0.10 + */ + public static void writeJpg(Image image, OutputStream out) throws IORuntimeException { + write(image, IMAGE_TYPE_JPG, out); + } + + /** + * 写出图像为PNG格式 + * + * @param image {@link Image} + * @param out 写出到的目标流 + * @throws IORuntimeException IO异常 + * @since 4.0.10 + */ + public static void writePng(Image image, OutputStream out) throws IORuntimeException { + write(image, IMAGE_TYPE_PNG, out); + } + + /** + * 写出图像 + * + * @param image {@link Image} + * @param imageType 图片类型(图片扩展名) + * @param out 写出到的目标流 + * @throws IORuntimeException IO异常 + * @since 3.1.2 + */ + public static void write(Image image, String imageType, OutputStream out) throws IORuntimeException { + write(image, imageType, getImageOutputStream(out)); + } + + /** + * 写出图像为指定格式 + * + * @param image {@link Image} + * @param imageType 图片类型(图片扩展名) + * @param destImageStream 写出到的目标流 + * @return 是否成功写出,如果返回false表示未找到合适的Writer + * @throws IORuntimeException IO异常 + * @since 3.1.2 + */ + public static boolean write(Image image, String imageType, ImageOutputStream destImageStream) throws IORuntimeException { + return write(image, imageType, destImageStream, 1); + } + + /** + * 写出图像为指定格式 + * + * @param image {@link Image} + * @param imageType 图片类型(图片扩展名) + * @param destImageStream 写出到的目标流 + * @param quality 质量,数字为0~1(不包括0和1)表示质量压缩比,除此数字外设置表示不压缩 + * @return 是否成功写出,如果返回false表示未找到合适的Writer + * @throws IORuntimeException IO异常 + * @since 4.3.2 + */ + public static boolean write(Image image, String imageType, ImageOutputStream destImageStream, float quality) throws IORuntimeException { + if (StrUtil.isBlank(imageType)) { + imageType = IMAGE_TYPE_JPG; + } + + final ImageWriter writer = getWriter(image, imageType); + return write(toBufferedImage(image, imageType), writer, destImageStream, quality); + } + + /** + * 写出图像为目标文件扩展名对应的格式 + * + * @param image {@link Image} + * @param targetFile 目标文件 + * @throws IORuntimeException IO异常 + * @since 3.1.0 + */ + public static void write(Image image, File targetFile) throws IORuntimeException { + ImageOutputStream out = null; + try { + out = getImageOutputStream(targetFile); + write(image, FileUtil.extName(targetFile), out); + } finally { + IoUtil.close(out); + } + } + + /** + * 通过{@link ImageWriter}写出图片到输出流 + * + * @param image 图片 + * @param writer {@link ImageWriter} + * @param output 输出的Image流{@link ImageOutputStream} + * @param quality 质量,数字为0~1(不包括0和1)表示质量压缩比,除此数字外设置表示不压缩 + * @return 是否成功写出 + * @since 4.3.2 + */ + public static boolean write(Image image, ImageWriter writer, ImageOutputStream output, float quality) { + if (writer == null) { + return false; + } + + writer.setOutput(output); + final RenderedImage renderedImage = toRenderedImage(image); + // 设置质量 + ImageWriteParam imgWriteParams = null; + if (quality > 0 && quality < 1) { + imgWriteParams = writer.getDefaultWriteParam(); + if (imgWriteParams.canWriteCompressed()) { + imgWriteParams.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + imgWriteParams.setCompressionQuality(quality); + final ColorModel colorModel = renderedImage.getColorModel();// ColorModel.getRGBdefault(); + imgWriteParams.setDestinationType(new ImageTypeSpecifier(colorModel, colorModel.createCompatibleSampleModel(16, 16))); + } + } + + try { + if (null != imgWriteParams) { + writer.write(null, new IIOImage(renderedImage, null, null), imgWriteParams); + } else { + writer.write(renderedImage); + } + output.flush(); + } catch (IOException e) { + throw new IORuntimeException(e); + } finally { + writer.dispose(); + } + return true; + } + + /** + * 获得{@link ImageReader} + * + * @param type 图片文件类型,例如 "jpeg" 或 "tiff" + * @return {@link ImageReader} + */ + public static ImageReader getReader(String type) { + final Iterator iterator = ImageIO.getImageReadersByFormatName(type); + if (iterator.hasNext()) { + return iterator.next(); + } + return null; + } + + /** + * 从文件中读取图片,请使用绝对路径,使用相对路径会相对于ClassPath + * + * @param imageFilePath 图片文件路径 + * @return 图片 + * @since 4.1.15 + */ + public static BufferedImage read(String imageFilePath) { + return read(FileUtil.file(imageFilePath)); + } + + /** + * 从文件中读取图片 + * + * @param imageFile 图片文件 + * @return 图片 + * @since 3.2.2 + */ + public static BufferedImage read(File imageFile) { + try { + return ImageIO.read(imageFile); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 从{@link Resource}中读取图片 + * + * @param resource 图片资源 + * @return 图片 + * @since 4.4.1 + */ + public static BufferedImage read(Resource resource) { + return read(resource.getStream()); + } + + /** + * 从流中读取图片 + * + * @param imageStream 图片文件 + * @return 图片 + * @since 3.2.2 + */ + public static BufferedImage read(InputStream imageStream) { + try { + return ImageIO.read(imageStream); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 从图片流中读取图片 + * + * @param imageStream 图片文件 + * @return 图片 + * @since 3.2.2 + */ + public static BufferedImage read(ImageInputStream imageStream) { + try { + return ImageIO.read(imageStream); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 从URL中读取图片 + * + * @param imageUrl 图片文件 + * @return 图片 + * @since 3.2.2 + */ + public static BufferedImage read(URL imageUrl) { + try { + return ImageIO.read(imageUrl); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 获取{@link ImageOutputStream} + * + * @param out {@link OutputStream} + * @return {@link ImageOutputStream} + * @throws IORuntimeException IO异常 + * @since 3.1.2 + */ + public static ImageOutputStream getImageOutputStream(OutputStream out) throws IORuntimeException { + try { + return ImageIO.createImageOutputStream(out); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 获取{@link ImageOutputStream} + * + * @param outFile {@link File} + * @return {@link ImageOutputStream} + * @throws IORuntimeException IO异常 + * @since 3.2.2 + */ + public static ImageOutputStream getImageOutputStream(File outFile) throws IORuntimeException { + try { + return ImageIO.createImageOutputStream(outFile); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 获取{@link ImageInputStream} + * + * @param in {@link InputStream} + * @return {@link ImageInputStream} + * @throws IORuntimeException IO异常 + * @since 3.1.2 + */ + public static ImageInputStream getImageInputStream(InputStream in) throws IORuntimeException { + try { + return ImageIO.createImageInputStream(in); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 根据给定的Image对象和格式获取对应的{@link ImageWriter},如果未找到合适的Writer,返回null + * + * @param img {@link Image} + * @param formatName 图片格式,例如"jpg"、"png" + * @return {@link ImageWriter} + * @since 4.3.2 + */ + public static ImageWriter getWriter(Image img, String formatName) { + final ImageTypeSpecifier type = ImageTypeSpecifier.createFromRenderedImage(toRenderedImage(img)); + final Iterator iter = ImageIO.getImageWriters(type, formatName); + return iter.hasNext() ? iter.next() : null; + } + + /** + * 根据给定的图片格式或者扩展名获取{@link ImageWriter},如果未找到合适的Writer,返回null + * + * @param formatName 图片格式或扩展名,例如"jpg"、"png" + * @return {@link ImageWriter} + * @since 4.3.2 + */ + public static ImageWriter getWriter(String formatName) { + ImageWriter writer = null; + Iterator iter = ImageIO.getImageWritersByFormatName(formatName); + if (iter.hasNext()) { + writer = iter.next(); + } + if (null == writer) { + // 尝试扩展名获取 + iter = ImageIO.getImageWritersBySuffix(formatName); + if (iter.hasNext()) { + writer = iter.next(); + } + } + return writer; + } + + // -------------------------------------------------------------------------------------------------------------------- Color + /** + * Color对象转16进制表示,例如#fcf6d6 + * + * @param color {@link Color} + * @return 16进制的颜色值,例如#fcf6d6 + * @since 4.1.14 + */ + public static String toHex(Color color) { + String R = Integer.toHexString(color.getRed()); + R = R.length() < 2 ? ('0' + R) : R; + String G = Integer.toHexString(color.getGreen()); + G = G.length() < 2 ? ('0' + G) : G; + String B = Integer.toHexString(color.getBlue()); + B = B.length() < 2 ? ('0' + B) : B; + return '#' + R + G + B; + } + + /** + * 16进制的颜色值转换为Color对象,例如#fcf6d6 + * + * @param hex 16进制的颜色值,例如#fcf6d6 + * @return {@link Color} + * @since 4.1.14 + */ + public static Color hexToColor(String hex) { + return getColor(Integer.parseInt(StrUtil.removePrefix(hex, "#"), 16)); + } + + /** + * 获取一个RGB值对应的颜色 + * + * @param rgb RGB值 + * @return {@link Color} + * @since 4.1.14 + */ + public static Color getColor(int rgb) { + return new Color(rgb); + } + + /** + * 将颜色值转换成具体的颜色类型 汇集了常用的颜色集,支持以下几种形式: + * + *
+	 * 1. 颜色的英文名(大小写皆可)
+	 * 2. 16进制表示,例如:#fcf6d6或者$fcf6d6
+	 * 3. RGB形式,例如:13,148,252
+	 * 
+ * + * 方法来自:com.lnwazg.kit + * + * @param colorName 颜色的英文名,16进制表示或RGB表示 + * @return {@link Color} + * @since 4.1.14 + */ + public static Color getColor(String colorName) { + if (StrUtil.isBlank(colorName)) { + return null; + } + colorName = colorName.toUpperCase(); + + if ("BLACK".equals(colorName)) { + return Color.BLACK; + } else if ("WHITE".equals(colorName)) { + return Color.WHITE; + } else if ("LIGHTGRAY".equals(colorName) || "LIGHT_GRAY".equals(colorName)) { + return Color.LIGHT_GRAY; + } else if ("GRAY".equals(colorName)) { + return Color.GRAY; + } else if ("DARK_GRAY".equals(colorName) || "DARK_GRAY".equals(colorName)) { + return Color.DARK_GRAY; + } else if ("RED".equals(colorName)) { + return Color.RED; + } else if ("PINK".equals(colorName)) { + return Color.PINK; + } else if ("ORANGE".equals(colorName)) { + return Color.ORANGE; + } else if ("YELLOW".equals(colorName)) { + return Color.YELLOW; + } else if ("GREEN".equals(colorName)) { + return Color.GREEN; + } else if ("MAGENTA".equals(colorName)) { + return Color.MAGENTA; + } else if ("CYAN".equals(colorName)) { + return Color.CYAN; + } else if ("BLUE".equals(colorName)) { + return Color.BLUE; + } else if ("DARKGOLD".equals(colorName)) { + // 暗金色 + return hexToColor("#9e7e67"); + } else if ("LIGHTGOLD".equals(colorName)) { + // 亮金色 + return hexToColor("#ac9c85"); + } else if (StrUtil.startWith(colorName, '#')) { + return hexToColor(colorName); + } else if (StrUtil.startWith(colorName, '$')) { + // 由于#在URL传输中无法传输,因此用$代替# + return hexToColor("#" + colorName.substring(1)); + } else { + // rgb值 + final List rgb = StrUtil.split(colorName, ','); + if (3 == rgb.size()) { + final Integer r = Convert.toInt(rgb.get(0)); + final Integer g = Convert.toInt(rgb.get(1)); + final Integer b = Convert.toInt(rgb.get(2)); + if (false == ArrayUtil.hasNull(r, g, b)) { + return new Color(r, g, b); + } + } else { + return null; + } + } + return null; + } + + /** + * 生成随机颜色 + * + * @return 随机颜色 + * @since 3.1.2 + */ + public static Color randomColor() { + return randomColor(null); + } + + /** + * 生成随机颜色 + * + * @param random 随机对象 {@link Random} + * @return 随机颜色 + * @since 3.1.2 + */ + public static Color randomColor(Random random) { + if (null == random) { + random = RandomUtil.getRandom(); + } + return new Color(random.nextInt(255), random.nextInt(255), random.nextInt(255)); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/img/ScaleType.java b/hutool-core/src/main/java/cn/hutool/core/img/ScaleType.java new file mode 100644 index 000000000..e4e40dcd9 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/img/ScaleType.java @@ -0,0 +1,43 @@ +package cn.hutool.core.img; + +import java.awt.Image; + +/** + * 图片缩略算法类型 + * + * @author looly + * @since 4.5.8 + */ +public enum ScaleType { + + /** 默认 */ + DEFAULT(Image.SCALE_DEFAULT), + /** 快速 */ + FAST(Image.SCALE_FAST), + /** 平滑 */ + SMOOTH(Image.SCALE_SMOOTH), + /** 使用 ReplicateScaleFilter 类中包含的图像缩放算法 */ + REPLICATE(Image.SCALE_REPLICATE), + /** Area Averaging算法 */ + AREA_AVERAGING(Image.SCALE_AREA_AVERAGING); + + /** + * 构造 + * + * @param value 缩放方式 + * @see Image#SCALE_DEFAULT + * @see Image#SCALE_FAST + * @see Image#SCALE_SMOOTH + * @see Image#SCALE_REPLICATE + * @see Image#SCALE_AREA_AVERAGING + */ + private ScaleType(int value) { + this.value = value; + } + + private int value; + + public int getValue() { + return this.value; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/img/package-info.java b/hutool-core/src/main/java/cn/hutool/core/img/package-info.java new file mode 100644 index 000000000..2d3d5a934 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/img/package-info.java @@ -0,0 +1,7 @@ +/** + * 图像处理相关工具类封装 + * + * @author looly + * + */ +package cn.hutool.core.img; \ No newline at end of file diff --git a/hutool-core/src/main/java/cn/hutool/core/io/BOMInputStream.java b/hutool-core/src/main/java/cn/hutool/core/io/BOMInputStream.java new file mode 100644 index 000000000..29d678e78 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/BOMInputStream.java @@ -0,0 +1,117 @@ +package cn.hutool.core.io; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PushbackInputStream; + +import cn.hutool.core.util.CharsetUtil; + +/** + * 读取带BOM头的流内容,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 + */ +public class BOMInputStream extends InputStream { + PushbackInputStream in; + boolean isInited = false; + String defaultCharset; + String charset; + + private static final int BOM_SIZE = 4; + + // ----------------------------------------------------------------- Constructor start + public BOMInputStream(InputStream in) { + this(in, CharsetUtil.UTF_8); + } + + public BOMInputStream(InputStream in, String defaultCharset) { + this.in = new PushbackInputStream(in, BOM_SIZE); + this.defaultCharset = defaultCharset; + } + // ----------------------------------------------------------------- Constructor end + + public String getDefaultCharset() { + return defaultCharset; + } + + 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; + } + // System.out.println("read=" + n + ", unread=" + unread); + + if (unread > 0) { + in.unread(bom, (n - unread), unread); + } + + isInited = true; + } +} \ No newline at end of file diff --git a/hutool-core/src/main/java/cn/hutool/core/io/BufferUtil.java b/hutool-core/src/main/java/cn/hutool/core/io/BufferUtil.java new file mode 100644 index 000000000..fb15159da --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/BufferUtil.java @@ -0,0 +1,251 @@ +package cn.hutool.core.io; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; + +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; + +/** + * {@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)); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/io/FastByteArrayOutputStream.java b/hutool-core/src/main/java/cn/hutool/core/io/FastByteArrayOutputStream.java new file mode 100644 index 000000000..761238575 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/FastByteArrayOutputStream.java @@ -0,0 +1,114 @@ +package cn.hutool.core.io; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; + +import cn.hutool.core.util.CharsetUtil; + +/** + * 基于快速缓冲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() throws IOException{ + // nop + } + + public void reset() { + buffer.reset(); + } + + /** + * 写出 + * @param out 输出流 + * @throws IORuntimeException IO异常 + */ + public void writeTo(OutputStream out) throws IORuntimeException { + final int index = buffer.index(); + 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 new String(toByteArray()); + } + + /** + * 转为字符串 + * @param charsetName 编码 + * @return 字符串 + */ + public String toString(String charsetName) { + return toString(CharsetUtil.charset(charsetName)); + } + + /** + * 转为字符串 + * @param charset 编码 + * @return 字符串 + */ + public String toString(Charset charset) { + return new String(toByteArray(), charset); + } + +} \ No newline at end of file diff --git a/hutool-core/src/main/java/cn/hutool/core/io/FastByteBuffer.java b/hutool-core/src/main/java/cn/hutool/core/io/FastByteBuffer.java new file mode 100644 index 000000000..efdf6cba9 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/FastByteBuffer.java @@ -0,0 +1,285 @@ +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.minChunkLen = 1024; + } + + public FastByteBuffer(int size) { + 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; + } + } + +} \ No newline at end of file diff --git a/hutool-core/src/main/java/cn/hutool/core/io/FileTypeUtil.java b/hutool-core/src/main/java/cn/hutool/core/io/FileTypeUtil.java new file mode 100644 index 000000000..b2308de55 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/FileTypeUtil.java @@ -0,0 +1,159 @@ +package cn.hutool.core.io; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; + +import cn.hutool.core.util.StrUtil; + +/** + * 文件类型判断工具类 + * + *

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

+ * + *

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

+ * + * @author Looly + * + */ +public final class FileTypeUtil { + + private FileTypeUtil() { + }; + + private static final Map fileTypeMap; + + static { + fileTypeMap = new ConcurrentHashMap<>(); + + fileTypeMap.put("ffd8ff", "jpg"); // JPEG (jpg) + fileTypeMap.put("89504e47", "png"); // PNG (png) + fileTypeMap.put("4749463837", "gif"); // GIF (gif) + fileTypeMap.put("4749463839", "gif"); // GIF (gif) + fileTypeMap.put("49492a00227105008037", "tif"); // TIFF (tif) + fileTypeMap.put("424d228c010000000000", "bmp"); // 16色位图(bmp) + fileTypeMap.put("424d8240090000000000", "bmp"); // 24位位图(bmp) + fileTypeMap.put("424d8e1b030000000000", "bmp"); // 256色位图(bmp) + fileTypeMap.put("41433130313500000000", "dwg"); // CAD (dwg) + fileTypeMap.put("7b5c727466315c616e73", "rtf"); // Rich Text Format (rtf) + fileTypeMap.put("38425053000100000000", "psd"); // Photoshop (psd) + fileTypeMap.put("46726f6d3a203d3f6762", "eml"); // Email [Outlook Express 6] (eml) + fileTypeMap.put("d0cf11e0a1b11ae10000", "doc"); // MS Excel 注意:word、msi 和 excel的文件头一样 + fileTypeMap.put("d0cf11e0a1b11ae10000", "vsd"); // Visio 绘图 + fileTypeMap.put("5374616E64617264204A", "mdb"); // MS Access (mdb) + fileTypeMap.put("252150532D41646F6265", "ps"); + fileTypeMap.put("255044462d312e", "pdf"); // Adobe Acrobat (pdf) + fileTypeMap.put("2e524d46000000120001", "rmvb"); // rmvb/rm相同 + fileTypeMap.put("464c5601050000000900", "flv"); // flv与f4v相同 + fileTypeMap.put("00000020667479706d70", "mp4"); + fileTypeMap.put("49443303000000002176", "mp3"); + fileTypeMap.put("000001ba210001000180", "mpg"); // + fileTypeMap.put("3026b2758e66cf11a6d9", "wmv"); // wmv与asf相同 + fileTypeMap.put("52494646e27807005741", "wav"); // Wave (wav) + fileTypeMap.put("52494646d07d60074156", "avi"); + fileTypeMap.put("4d546864000000060001", "mid"); // MIDI (mid) + fileTypeMap.put("526172211a0700cf9073", "rar");// WinRAR + fileTypeMap.put("235468697320636f6e66", "ini"); + fileTypeMap.put("504B03040a0000000000", "jar"); + fileTypeMap.put("504B0304140008000800", "jar"); + fileTypeMap.put("D0CF11E0A1B11AE10", "xls");// xls文件 + fileTypeMap.put("504B0304", "zip"); + fileTypeMap.put("4d5a9000030000000400", "exe");// 可执行文件 + fileTypeMap.put("3c25402070616765206c", "jsp");// jsp文件 + fileTypeMap.put("4d616e69666573742d56", "mf");// MF文件 + fileTypeMap.put("7061636b616765207765", "java");// java文件 + fileTypeMap.put("406563686f206f66660d", "bat");// bat文件 + fileTypeMap.put("1f8b0800000000000000", "gz");// gz文件 + fileTypeMap.put("cafebabe0000002e0041", "class");// bat文件 + fileTypeMap.put("49545346030000006000", "chm");// bat文件 + fileTypeMap.put("04000000010000001300", "mxp");// bat文件 + fileTypeMap.put("d0cf11e0a1b11ae10000", "wps");// WPS文字wps、表格et、演示dps都是一样的 + fileTypeMap.put("6431303a637265617465", "torrent"); + fileTypeMap.put("6D6F6F76", "mov"); // Quicktime (mov) + fileTypeMap.put("FF575043", "wpd"); // WordPerfect (wpd) + fileTypeMap.put("CFAD12FEC5FD746F", "dbx"); // Outlook Express (dbx) + fileTypeMap.put("2142444E", "pst"); // Outlook (pst) + fileTypeMap.put("AC9EBD8F", "qdf"); // Quicken (qdf) + fileTypeMap.put("E3828596", "pwl"); // Windows Password (pwl) + fileTypeMap.put("2E7261FD", "ram"); // Real Audio (ram) + } + + /** + * 增加文件类型映射
+ * 如果已经存在将覆盖之前的映射 + * + * @param fileStreamHexHead 文件流头部Hex信息 + * @param extName 文件扩展名 + * @return 之前已经存在的文件扩展名 + */ + public static String putFileType(String fileStreamHexHead, String extName) { + return fileTypeMap.put(fileStreamHexHead.toLowerCase(), extName); + } + + /** + * 移除文件类型映射 + * + * @param fileStreamHexHead 文件流头部Hex信息 + * @return 移除的文件扩展名 + */ + public static String removeFileType(String fileStreamHexHead) { + return fileTypeMap.remove(fileStreamHexHead.toLowerCase()); + } + + /** + * 根据文件流的头部信息获得文件类型 + * + * @param fileStreamHexHead 文件流头部16进制字符串 + * @return 文件类型,未找到为null + */ + public static String getType(String fileStreamHexHead) { + for (Entry fileTypeEntry : fileTypeMap.entrySet()) { + if(StrUtil.startWithIgnoreCase(fileStreamHexHead, fileTypeEntry.getKey())) { + return fileTypeEntry.getValue(); + } + } + return null; + } + + /** + * 根据文件流的头部信息获得文件类型 + * + * @param in {@link InputStream} + * @return 类型,文件的扩展名,未找到为null + * @throws IORuntimeException 读取流引起的异常 + */ + public static String getType(InputStream in) throws IORuntimeException { + return getType(IoUtil.readHex28Upper(in)); + } + + /** + * 根据文件流的头部信息获得文件类型 + * + * @param file 文件 {@link File} + * @return 类型,文件的扩展名,未找到为null + * @throws IORuntimeException 读取文件引起的异常 + */ + public static String getType(File file) throws IORuntimeException { + FileInputStream in = null; + try { + in = IoUtil.toStream(file); + return getType(in); + } finally { + IoUtil.close(in); + } + } + + /** + * 通过路径获得文件类型 + * + * @param path 路径,绝对路径或相对ClassPath的路径 + * @return 类型 + * @throws IORuntimeException 读取文件引起的异常 + */ + public static String getTypeByPath(String path) throws IORuntimeException { + return getType(FileUtil.file(path)); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/io/FileUtil.java b/hutool-core/src/main/java/cn/hutool/core/io/FileUtil.java new file mode 100644 index 000000000..d9a53c818 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/FileUtil.java @@ -0,0 +1,3516 @@ +package cn.hutool.core.io; + +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.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +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.CopyOption; +import java.nio.file.DirectoryStream; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.regex.Pattern; +import java.util.zip.CRC32; +import java.util.zip.Checksum; + +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.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.Tailer; +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.lang.Assert; +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; + +/** + * 文件工具类 + * + * @author xiaoleilu + * + */ +public class FileUtil { + + /** 类Unix路径分隔符 */ + private static final char UNIX_SEPARATOR = CharUtil.SLASH; + /** Windows路径分隔符 */ + private static final char WINDOWS_SEPARATOR = CharUtil.BACKSLASH; + /** Windows下文件名中的无效字符 */ + private static Pattern FILE_NAME_INVALID_PATTERN_WIN = Pattern.compile("[\\\\/:*?\"<>|]"); + + /** Class文件扩展名 */ + public static final String CLASS_EXT = ".class"; + /** Jar文件扩展名 */ + public static final String JAR_FILE_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; + + /** + * 是否为Windows环境 + * + * @return 是否为Windows环境 + * @since 3.0.9 + */ + public static boolean isWindows() { + return WINDOWS_SEPARATOR == File.separatorChar; + } + + /** + * 列出目录文件
+ * 给定的绝对路径不能是压缩包中的路径 + * + * @param path 目录绝对路径或者相对路径 + * @return 文件列表(包含目录) + */ + public static File[] ls(String path) { + if (path == null) { + return null; + } + + path = getAbsolutePath(path); + + 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) { + return true; + } + + if (file.isDirectory()) { + String[] subFiles = file.list(); + if (ArrayUtil.isEmpty(subFiles)) { + return true; + } + } else if (file.isFile()) { + return file.length() <= 0; + } + + return false; + } + + /** + * 目录是否为空 + * + * @param file 目录 + * @return 是否为空,当提供非目录时,返回false + */ + public static boolean isNotEmpty(File file) { + return false == isEmpty(file); + } + + /** + * 目录是否为空 + * + * @param dirPath 目录 + * @return 是否为空 + * @exception IORuntimeException IOException + */ + public static boolean isDirEmpty(Path dirPath) { + try (DirectoryStream dirStream = Files.newDirectoryStream(dirPath)) { + return false == dirStream.iterator().hasNext(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 目录是否为空 + * + * @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) { + List fileList = new ArrayList(); + if (null == file) { + return fileList; + } else if (false == file.exists()) { + return fileList; + } + + if (file.isDirectory()) { + final File[] subFiles = file.listFiles(); + if (ArrayUtil.isNotEmpty(subFiles)) { + for (File tmp : subFiles) { + fileList.addAll(loopFiles(tmp, fileFilter)); + } + } + } else { + if (null == fileFilter || fileFilter.accept(file)) { + fileList.add(file); + } + } + + return fileList; + } + + /** + * 递归遍历目录以及子目录中的所有文件 + * + * @param path 当前遍历文件或目录的路径 + * @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); + } + + /** + * 获得指定目录下所有文件
+ * 不会扫描子目录 + * + * @param path 相对ClassPath的目录或者绝对路径目录 + * @return 文件路径列表(如果是jar中的文件,则给定类似.jar!/xxx/xxx的路径) + * @throws IORuntimeException IO异常 + */ + public static List listFileNames(String path) throws IORuntimeException { + if (path == null) { + return null; + } + List paths = new ArrayList(); + + int index = path.lastIndexOf(FileUtil.JAR_PATH_EXT); + if (index == -1) { + // 普通目录路径 + File[] files = ls(path); + for (File file : files) { + if (file.isFile()) { + paths.add(file.getName()); + } + } + } else { + // jar文件 + path = getAbsolutePath(path); + if (false == StrUtil.endWith(path, UNIX_SEPARATOR)) { + path = path + UNIX_SEPARATOR; + } + // jar文件中的路径 + index = index + FileUtil.JAR_FILE_EXT.length(); + JarFile jarFile = null; + try { + jarFile = new JarFile(path.substring(0, index)); + final String subPath = path.substring(index + 2); + for (JarEntry entry : Collections.list(jarFile.entries())) { + final String name = entry.getName(); + if (name.startsWith(subPath)) { + final String nameSuffix = StrUtil.removePrefix(name, subPath); + if (false == StrUtil.contains(nameSuffix, UNIX_SEPARATOR)) { + paths.add(nameSuffix); + } + } + } + } catch (IOException e) { + throw new IORuntimeException(StrUtil.format("Can not read file path of [{}]", path), e); + } finally { + IoUtil.close(jarFile); + } + } + return paths; + } + + /** + * 创建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 文件路径 + * @return File + */ + public static File file(String path) { + if (StrUtil.isBlank(path)) { + throw new NullPointerException("File path is blank!"); + } + 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对象
+ * 此方法会检查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, new File(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, "directorydirectory 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; + } + + /** + * 通过多层目录创建文件 + * + * 元素名(多层目录名) + * + * @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 (path == null) ? false : file(path).exists(); + } + + /** + * 判断文件是否存在,如果file为null,则返回false + * + * @param file 文件 + * @return 如果存在返回true + */ + public static boolean exist(File file) { + return (file == null) ? false : file.exists(); + } + + /** + * 是否存在匹配文件 + * + * @param directory 文件夹路径 + * @param regexp 文件夹中所包含文件名的正则表达式 + * @return 如果存在匹配文件返回true + */ + public static boolean exist(String directory, String regexp) { + final File file = new File(directory); + if (false == 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 目录或文件 + * @return 总大小,bytes长度 + */ + public static long size(File file) { + Assert.notNull(file, "file argument is null !"); + if (false == file.exists()) { + throw new IllegalArgumentException(StrUtil.format("File [{}] not exist !", file.getAbsolutePath())); + } + + if (file.isDirectory()) { + long size = 0L; + File[] subFiles = file.listFiles(); + if (ArrayUtil.isEmpty(subFiles)) { + return 0L;// empty directory + } + for (int i = 0; i < subFiles.length; i++) { + size += size(subFiles[i]); + } + return size; + } else { + return file.length(); + } + } + + /** + * 给定文件或目录的最后修改时间是否晚于给定时间 + * + * @param file 文件或目录 + * @param reference 参照文件 + * @return 是否晚于给定时间 + */ + public static boolean newerThan(File file, File reference) { + if (null == reference || false == 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 || false == file.exists()) { + return false;// 不存在的文件一定比任何时间旧 + } + return file.lastModified() > timeMillis; + } + + /** + * 创建文件及其父目录,如果这个文件存在,直接返回这个文件
+ * 此方法不对File对象类型做判断,如果File不存在,无法判断其类型 + * + * @param fullFilePath 文件的全路径,使用POSIX风格 + * @return 文件,若路径为null,返回null + * @throws IORuntimeException IO异常 + */ + public static File touch(String fullFilePath) throws IORuntimeException { + if (fullFilePath == null) { + return null; + } + return touch(file(fullFilePath)); + } + + /** + * 创建文件及其父目录,如果这个文件存在,直接返回这个文件
+ * 此方法不对File对象类型做判断,如果File不存在,无法判断其类型 + * + * @param file 文件对象 + * @return 文件,若路径为null,返回null + * @throws IORuntimeException IO异常 + */ + public static File touch(File file) throws IORuntimeException { + if (null == file) { + return null; + } + if (false == file.exists()) { + mkParentDirs(file); + try { + 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) { + final File parentFile = file.getParentFile(); + if (null != parentFile && false == parentFile.exists()) { + parentFile.mkdirs(); + } + return parentFile; + } + + /** + * 创建父文件夹,如果存在直接返回此文件夹 + * + * @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)); + } + + /** + * 删除文件或者文件夹
+ * 注意:删除文件夹时不会判断文件夹是否为空,如果不空则递归删除子文件或文件夹
+ * 某个文件删除失败会终止删除操作 + * + * @param file 文件对象 + * @return 成功与否 + * @throws IORuntimeException IO异常 + */ + public static boolean del(File file) throws IORuntimeException { + if (file == null || false == file.exists()) { + // 如果文件不存在或已被删除,此处返回true表示删除成功 + return true; + } + + if (file.isDirectory()) { + // 清空目录下所有文件和目录 + boolean isOk = clean(file); + if (false == isOk) { + return false; + } + } + + // 删除文件或清空后的目录 + return file.delete(); + } + + /** + * 删除文件或者文件夹
+ * 注意:删除文件夹时不会判断文件夹是否为空,如果不空则递归删除子文件或文件夹
+ * 某个文件删除失败会终止删除操作 + * + * @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 (Files.isDirectory(path)) { + Files.walkFileTree(path, new SimpleFileVisitor() { + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException { + if (e == null) { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } else { + throw e; + } + } + }); + } else { + Files.delete(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() == false || false == directory.isDirectory()) { + return true; + } + + final File[] files = directory.listFiles(); + boolean isOk; + for (File childFile : files) { + isOk = del(childFile); + if (isOk == false) { + // 删除一个出错则本次删除任务失败 + 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 || false == directory.exists() || false == directory.isDirectory()) { + return true; + } + + final File[] files = directory.listFiles(); + if(ArrayUtil.isEmpty(files)) { + //空文件夹则删除之 + 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 (false == dir.exists()) { + dir.mkdirs(); + } + return dir; + } + + /** + * 创建临时文件
+ * 创建后的文件名为 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 + * + * @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, dir).getCanonicalFile(); + if (isReCreat) { + file.delete(); + file.createNewFile(); + } + return file; + } catch (IOException ioex) { // fixes java.io.WinNTFileSystem.createFileExclusively access denied + if (++exceptionsCount >= 50) { + throw new IORuntimeException(ioex); + } + } + } + } + + /** + * 通过JDK7+的 {@link 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.notNull(src, "Destination File path is null !"); + return copyFile(Paths.get(src), Paths.get(dest), options).toFile(); + } + + /** + * 通过JDK7+的 {@link 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 (false == 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(); + } + + /** + * 通过JDK7+的 {@link Files#copy(Path, Path, CopyOption...)} 方法拷贝文件 + * + * @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 { + Assert.notNull(src, "Source File is null !"); + Assert.notNull(dest, "Destination File or directiory is null !"); + + Path destPath = dest.toFile().isDirectory() ? dest.resolve(src.getFileName()) : dest; + try { + return Files.copy(src, destPath, options); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 复制文件或目录
+ * 如果目标文件为目录,则将源文件以相同文件名拷贝到目标目录 + * + * @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 dest 目标文件或者目录 + * @param isOverride 是否覆盖目标,只有目标为文件才覆盖 + * @throws IORuntimeException IO异常 + */ + public static void move(File src, File dest, boolean isOverride) throws IORuntimeException { + // check + if (false == src.exists()) { + throw new IORuntimeException("File not found: " + src); + } + + // 来源为文件夹,目标为文件 + if (src.isDirectory() && dest.isFile()) { + throw new IORuntimeException(StrUtil.format("Can not move directory [{}] to file [{}]", src, dest)); + } + + if (isOverride && dest.isFile()) {// 只有目标为文件的情况下覆盖之 + dest.delete(); + } + + // 来源为文件,目标为文件夹 + if (src.isFile() && dest.isDirectory()) { + dest = new File(dest, src.getName()); + } + + if (false == src.renameTo(dest)) { + // 在文件系统不同的情况下,renameTo会失败,此时使用copy,然后删除原文件 + try { + copy(src, dest, isOverride); + } catch (Exception e) { + throw new IORuntimeException(StrUtil.format("Move [{}] to [{}] failed!", src, dest), e); + } + // 复制后删除源 + del(src); + } + } + + /** + * 修改文件或目录的文件名,不变更路径,只是简单修改文件名
+ * 重命名有两种模式:
+ * 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 目标文件 + * @since 3.0.9 + */ + public static File rename(File file, String newName, boolean isRetainExt, boolean isOverride) { + if (isRetainExt) { + newName = newName.concat(".").concat(FileUtil.extName(file)); + } + final Path path = file.toPath(); + final CopyOption[] options = isOverride ? new CopyOption[] { StandardCopyOption.REPLACE_EXISTING } : new CopyOption[] {}; + try { + return Files.move(path, path.resolveSibling(newName), options).toFile(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 获取规范的绝对路径 + * + * @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(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)}方法标准化路径后判断 + * + * @param path 需要检查的Path + * @return 是否已经是绝对路径 + */ + public static boolean isAbsolutePath(String path) { + if (StrUtil.isEmpty(path)) { + return false; + } + + if (StrUtil.C_SLASH == path.charAt(0) || path.matches("^[a-zA-Z]:[/\\\\].*")) { + // 给定的路径已经是绝对路径了 + return true; + } + return false; + } + + /** + * 判断是否为目录,如果path为null,则返回false + * + * @param path 文件路径 + * @return 如果为目录true + */ + public static boolean isDirectory(String path) { + return (path == null) ? false : file(path).isDirectory(); + } + + /** + * 判断是否为目录,如果file为null,则返回false + * + * @param file 文件 + * @return 如果为目录true + */ + public static boolean isDirectory(File file) { + return (file == null) ? false : file.isDirectory(); + } + + /** + * 判断是否为目录,如果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); + } + + /** + * 判断是否为文件,如果path为null,则返回false + * + * @param path 文件路径 + * @return 如果为文件true + */ + public static boolean isFile(String path) { + return (path == null) ? false : file(path).isFile(); + } + + /** + * 判断是否为文件,如果file为null,则返回false + * + * @param file 文件 + * @return 如果为文件true + */ + public static boolean isFile(File file) { + return (file == null) ? false : file.isFile(); + } + + /** + * 判断是否为文件,如果file为null,则返回false + * + * @param path 文件 + * @param isFollowLinks 是否跟踪软链(快捷方式) + * @return 如果为文件true + */ + 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); + } + + /** + * 检查两个文件是否是同一个文件
+ * 所谓文件相同,是指File对象是否指向同一个文件或文件夹 + * + * @param file1 文件1 + * @param file2 文件2 + * @return 是否相同 + * @throws IORuntimeException IO异常 + * @see Files#isSameFile(Path, Path) + */ + public static boolean equals(File file1, File file2) throws IORuntimeException { + Assert.notNull(file1); + Assert.notNull(file2); + if (false == file1.exists() || false == file2.exists()) { + // 两个文件都不存在判断其路径是否相同 + if (false == file1.exists() && false == file2.exists() && pathEquals(file1, file2)) { + return true; + } + // 对于一个存在一个不存在的情况,一定不相同 + return false; + } + try { + return Files.isSameFile(file1.toPath(), file2.toPath()); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 比较两个文件内容是否相同
+ * 首先比较长度,长度一致再比较内容
+ * 此方法来自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 (false == 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 + * @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 是否被改动 + */ + public static boolean isModifed(File file, long lastModifyTime) { + if (null == file || false == 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"
+	 * 
+ * + * @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); + // 统一使用斜杠 + pathToUse = pathToUse.replaceAll("[/\\\\]{1,}", StrUtil.SLASH).trim(); + + int prefixIndex = pathToUse.indexOf(StrUtil.COLON); + String prefix = ""; + if (prefixIndex > -1) { + // 可能Windows风格路径 + prefix = pathToUse.substring(0, prefixIndex + 1); + if (StrUtil.startWith(prefix, StrUtil.C_SLASH)) { + // 去除类似于/C:这类路径开头的斜杠 + prefix = prefix.substring(1); + } + if (false == 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)) { + // 当前目录,丢弃 + } else if (StrUtil.DOUBLE_DOT.equals(element)) { + tops++; + } else { + if (tops > 0) { + // 有上级目录标记时按照个数依次跳过 + tops--; + } else { + // Normal path element found. + pathElements.add(0, element); + } + } + } + + 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; + } + + /** + * 获取指定位置的子路径部分,支持负数,例如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); + } + + // -------------------------------------------------------------------------------------------- name start + /** + * 返回文件名 + * + * @param file 文件 + * @return 文件名 + * @since 4.1.13 + */ + public static String getName(File file) { + return (null != file) ? file.getName() : null; + } + + /** + * 返回文件名 + * + * @param filePath 文件 + * @return 文件名 + * @since 4.1.13 + */ + public static String getName(String filePath) { + if (null == filePath) { + return filePath; + } + 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 主文件名 + */ + 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 fileName; + } + int len = fileName.length(); + if (0 == len) { + return fileName; + } + if (CharUtil.isFileSeparator(fileName.charAt(len - 1))) { + len--; + } + + int begin = 0; + int end = len; + char c; + for (int i = len - 1; i > -1; i--) { + c = fileName.charAt(i); + if (len == end && CharUtil.DOT == c) { + // 查找最后一个文件名和扩展名的分隔符:. + end = i; + } + if (0 == begin || begin > end) { + 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; + } + int index = fileName.lastIndexOf(StrUtil.DOT); + if (index == -1) { + return StrUtil.EMPTY; + } else { + String ext = fileName.substring(index + 1); + // 扩展名中不能包含路径相关的符号 + return StrUtil.containsAny(ext, UNIX_SEPARATOR, WINDOWS_SEPARATOR) ? StrUtil.EMPTY : ext; + } + } + // -------------------------------------------------------------------------------------------- name end + + /** + * 判断文件路径是否有指定后缀,忽略大小写
+ * 常用语判断扩展名 + * + * @param file 文件或目录 + * @param suffix 后缀 + * @return 是否有指定后缀 + */ + public static boolean pathEndsWith(File file, String suffix) { + return file.getPath().toLowerCase().endsWith(suffix); + } + + /** + * 根据文件流的头部信息获得文件类型 + * + * @see FileTypeUtil#getType(File) + * + * @param file 文件 {@link File} + * @return 类型,文件的扩展名,未找到为null + * @throws IORuntimeException IO异常 + */ + public static String getType(File file) throws IORuntimeException { + return FileTypeUtil.getType(file); + } + + /** + * 获取文件属性 + * + * @param path 文件路径{@link Path} + * @param isFollowLinks 是否跟踪到软链对应的真实路径 + * @return {@link BasicFileAttributes} + * @throws IORuntimeException IO异常 + * @since 3.1.0 + */ + 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); + } + } + + // -------------------------------------------------------------------------------------------- in start + /** + * 获得输入流 + * + * @param path Path + * @return 输入流 + * @throws IORuntimeException 文件未找到 + * @since 4.0.0 + */ + public static BufferedInputStream getInputStream(Path path) throws IORuntimeException { + try { + return new BufferedInputStream(Files.newInputStream(path)); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 获得输入流 + * + * @param file 文件 + * @return 输入流 + * @throws IORuntimeException 文件未找到 + */ + public static BufferedInputStream getInputStream(File file) throws IORuntimeException { + return new BufferedInputStream(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(new FileInputStream(file)); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 获得一个文件读取器 + * + * @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 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 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); + } + + /** + * 获得一个文件读取器 + * + * @param file 文件 + * @param charsetName 字符集 + * @return BufferedReader对象 + * @throws IORuntimeException IO异常 + */ + public static BufferedReader getReader(File file, String charsetName) throws IORuntimeException { + return IoUtil.getReader(getInputStream(file), 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异常 + */ + public static BufferedReader getReader(String path, String charsetName) throws IORuntimeException { + return getReader(file(path), 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异常 + */ + 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异常 + */ + public static String readString(String path, String charsetName) throws IORuntimeException { + return readString(file(path), 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 charset 字符集 + * @return 内容 + * @throws IORuntimeException IO异常 + */ + public static String readString(URL url, String 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异常 + */ + 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 charset 字符集 + * @return 文件中的每行内容的集合List + * @throws IORuntimeException IO异常 + */ + public static List readLines(URL url, String charset) throws IORuntimeException { + return readLines(url, charset, new ArrayList()); + } + + /** + * 从文件中读取每一行数据 + * + * @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 = null; + 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 = null; + 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 文件的绝对路径 + * @param charset 字符集 + * @return 从文件中load出的数据 + * @throws IORuntimeException IO异常 + * @deprecated 使用FileUtil#load(String, String, ReaderHandler) 代替 + */ + @Deprecated + public static T load(ReaderHandler readerHandler, String path, String charset) throws IORuntimeException { + return FileReader.create(file(path), CharsetUtil.charset(charset)).read(readerHandler); + } + + /** + * 按照给定的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 { + try { + return new BufferedOutputStream(new FileOutputStream(touch(file))); + } catch (Exception e) { + throw new IORuntimeException(e); + } + } + + /** + * 获得一个输出流对象 + * + * @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异常 + */ + public static BufferedWriter getWriter(String path, String charsetName, boolean isAppend) throws IORuntimeException { + return getWriter(touch(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异常 + */ + 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)); + } + + /** + * 获取当前系统的换行分隔符 + * + *
+	 * 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); + } + + /** + * 写数据到文件中 + * + * @param data 数据 + * @param path 目标文件 + * @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 FileWriter.create(dest).writeFromStream(in); + } + + /** + * 将流的内容写入文件
+ * + * @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 目标文件 + * @throws IORuntimeException IO异常 + */ + public static File writeToStream(File file, OutputStream out) throws IORuntimeException { + return FileReader.create(file).writeToStream(out); + } + + /** + * 将流的内容写入文件
+ * + * @param fullFilePath 文件绝对路径 + * @param out 输出流 + * @throws IORuntimeException IO异常 + */ + public static void writeToStream(String fullFilePath, OutputStream out) throws IORuntimeException { + 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 大小 + */ + public static String readableFileSize(long size) { + if (size <= 0) { + return "0"; + } + final String[] units = new String[] { "B", "kB", "MB", "GB", "TB", "EB" }; + int digitGroups = (int) (Math.log10(size) / Math.log10(1024)); + return new DecimalFormat("#,##0.##").format(size / Math.pow(1024, digitGroups)) + " " + units[digitGroups]; + } + + /** + * 转换文件编码
+ * 此方法用于转换文件编码,读取的文件实际编码必须与指定的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 清理后的文件名 + * @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) ? false : ReUtil.contains(FILE_NAME_INVALID_PATTERN_WIN, 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(new FileInputStream(file), checksum); + } catch (FileNotFoundException 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) { + throw new IORuntimeException(e); + } + if (false == 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) { + return URLConnection.getFileNameMap().getContentTypeFor(filePath); + } + + /** + * 判断是否为符号链接文件 + * + * @param file 被检查的文件 + * @return 是否为符号链接文件 + * @since 4.4.2 + */ + public static boolean isSymlink(File file) throws IORuntimeException { + return Files.isSymbolicLink(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 sub.toPath().startsWith(parent.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) { + FileUtil.tail(file, charset, Tailer.CONSOLE_HANDLER); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/io/IORuntimeException.java b/hutool-core/src/main/java/cn/hutool/core/io/IORuntimeException.java new file mode 100644 index 000000000..cf5713113 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/IORuntimeException.java @@ -0,0 +1,47 @@ +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) { + Throwable cause = this.getCause(); + if (null != cause && clazz.isInstance(cause)) { + return true; + } + return false; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/io/IoUtil.java b/hutool-core/src/main/java/cn/hutool/core/io/IoUtil.java new file mode 100644 index 000000000..a4fce6fc4 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/IoUtil.java @@ -0,0 +1,1139 @@ +package cn.hutool.core.io; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +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.ObjectInputStream; +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.ByteBuffer; +import java.nio.CharBuffer; +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; +import java.util.Collection; +import java.util.zip.CRC32; +import java.util.zip.CheckedInputStream; +import java.util.zip.Checksum; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.HexUtil; +import cn.hutool.core.util.StrUtil; + +/** + * IO工具类
+ * IO工具类只是辅助流的读写,并不负责关闭流。原因是流可能被多次读写,读写关闭后容易造成问题。 + * + * @author xiaoleilu + * + */ +public class IoUtil { + + /** 默认缓存大小 */ + public static final int DEFAULT_BUFFER_SIZE = 2048; + /** 默认中等缓存大小 */ + public static final int DEFAULT_MIDDLE_BUFFER_SIZE = 4096; + /** 默认大缓存大小 */ + public static final int DEFAULT_LARGE_BUFFER_SIZE = 8192; + + /** 数据流末尾 */ + public static final int EOF = -1; + + // -------------------------------------------------------------------------------------- Copy start + /** + * 将Reader中的内容复制到Writer中 使用默认缓存大小 + * + * @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中 + * + * @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中 + * + * @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 { + char[] buffer = new char[bufferSize]; + long size = 0; + int readSize; + if (null != streamProgress) { + streamProgress.start(); + } + try { + while ((readSize = reader.read(buffer, 0, bufferSize)) != EOF) { + writer.write(buffer, 0, readSize); + size += readSize; + writer.flush(); + if (null != streamProgress) { + streamProgress.progress(size); + } + } + } catch (Exception e) { + throw new IORuntimeException(e); + } + if (null != streamProgress) { + streamProgress.finish(); + } + return size; + } + + /** + * 拷贝流,使用默认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 { + Assert.notNull(in, "InputStream is null !"); + Assert.notNull(out, "OutputStream is null !"); + if (bufferSize <= 0) { + bufferSize = DEFAULT_BUFFER_SIZE; + } + + byte[] buffer = new byte[bufferSize]; + if (null != streamProgress) { + streamProgress.start(); + } + long size = 0; + try { + for (int readSize = -1; (readSize = in.read(buffer)) != EOF;) { + out.write(buffer, 0, readSize); + size += readSize; + out.flush(); + if (null != streamProgress) { + streamProgress.progress(size); + } + } + } catch (IOException e) { + throw new IORuntimeException(e); + } + if (null != streamProgress) { + streamProgress.finish(); + } + return size; + } + + /** + * 拷贝流 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 copy(Channels.newChannel(in), Channels.newChannel(out), bufferSize, streamProgress); + } + + /** + * 拷贝文件流,使用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!"); + + final FileChannel inChannel = in.getChannel(); + final FileChannel outChannel = out.getChannel(); + + try { + return inChannel.transferTo(0, inChannel.size(), outChannel); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 拷贝流,使用NIO,不会关闭流 + * + * @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,不会关闭流 + * + * @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,不会关闭流 + * + * @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 { + Assert.notNull(in, "InputStream is null !"); + Assert.notNull(out, "OutputStream is null !"); + + ByteBuffer byteBuffer = ByteBuffer.allocate(bufferSize <= 0 ? DEFAULT_BUFFER_SIZE : bufferSize); + long size = 0; + if (null != streamProgress) { + streamProgress.start(); + } + try { + while (in.read(byteBuffer) != EOF) { + byteBuffer.flip();// 写转读 + size += out.write(byteBuffer); + byteBuffer.clear(); + if (null != streamProgress) { + streamProgress.progress(size); + } + } + } catch (IOException e) { + throw new IORuntimeException(e); + } + if (null != streamProgress) { + streamProgress.finish(); + } + + return size; + } + // -------------------------------------------------------------------------------------- Copy end + + // -------------------------------------------------------------------------------------- getReader and getWriter start + /** + * 获得一个文件读取器 + * + * @param in 输入流 + * @param charsetName 字符集名称 + * @return BufferedReader对象 + */ + public static BufferedReader getReader(InputStream in, String charsetName) { + return getReader(in, Charset.forName(charsetName)); + } + + /** + * 获得一个Reader + * + * @param in 输入流 + * @param charset 字符集 + * @return BufferedReader对象 + */ + public static BufferedReader getReader(InputStream in, Charset charset) { + if (null == in) { + return null; + } + + InputStreamReader reader = null; + 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 + * + * @param out 输入流 + * @param charsetName 字符集 + * @return OutputStreamWriter对象 + */ + 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 + /** + * 从流中读取内容 + * + * @param in 输入流 + * @param charsetName 字符集 + * @return 内容 + * @throws IORuntimeException IO异常 + */ + public static String read(InputStream in, String charsetName) throws IORuntimeException { + 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 { + FastByteArrayOutputStream out = read(in); + return null == charset ? out.toString() : out.toString(charset); + } + + /** + * 从流中读取内容,读取完毕后并不关闭流 + * + * @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 in 输入流 + * @return 输出流 + * @throws IORuntimeException IO异常 + */ + public static FastByteArrayOutputStream read(InputStream in) throws IORuntimeException { + final FastByteArrayOutputStream out = new FastByteArrayOutputStream(); + copy(in, out); + return out; + } + + /** + * 从流中读取内容,读到输出流中 + * + * @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; + } + + /** + * 从Reader中读取String,读取完毕后并不关闭Reader + * + * @param reader Reader + * @return String + * @throws IORuntimeException IO异常 + */ + public static String read(Reader reader) 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().toString()); + } + } catch (IOException e) { + throw new IORuntimeException(e); + } + return builder.toString(); + } + + /** + * 从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); + } + + /** + * 从流中读取bytes + * + * @param in {@link InputStream} + * @return bytes + * @throws IORuntimeException IO异常 + */ + public static byte[] readBytes(InputStream in) throws IORuntimeException { + final FastByteArrayOutputStream out = new FastByteArrayOutputStream(); + copy(in, out); + return out.toByteArray(); + } + + /** + * 读取指定长度的byte数组,不关闭流 + * + * @param in {@link InputStream},为null返回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]; + } + + byte[] b = new byte[length]; + int readLength; + try { + readLength = in.read(b); + } catch (IOException e) { + throw new IORuntimeException(e); + } + if (readLength > 0 && readLength < length) { + byte[] b2 = new byte[length]; + System.arraycopy(b, 0, b2, 0, readLength); + return b2; + } else { + return b; + } + } + + /** + * 读取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); + } + + /** + * 从流中读取前28个byte并转换为16进制,字母部分使用大写 + * + * @param in {@link InputStream} + * @return 16进制字符串 + * @throws IORuntimeException IO异常 + */ + public static String readHex28Upper(InputStream in) throws IORuntimeException { + return readHex(in, 28, false); + } + + /** + * 从流中读取前28个byte并转换为16进制,字母部分使用小写 + * + * @param in {@link InputStream} + * @return 16进制字符串 + * @throws IORuntimeException IO异常 + */ + public static String readHex28Lower(InputStream in) throws IORuntimeException { + return readHex(in, 28, true); + } + + /** + * 从流中读取内容,读到输出流中 + * + * @param 读取对象的类型 + * @param in 输入流 + * @return 输出流 + * @throws IORuntimeException IO异常 + * @throws UtilException ClassNotFoundException包装 + */ + public static T readObj(InputStream in) throws IORuntimeException, UtilException { + if (in == null) { + throw new IllegalArgumentException("The InputStream must not be null"); + } + ObjectInputStream ois = null; + try { + ois = new ObjectInputStream(in); + @SuppressWarnings("unchecked") // may fail with CCE if serialised form is incorrect + final T obj = (T) ois.readObject(); + return obj; + } 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异常 + */ + 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, final T collection) throws IORuntimeException { + readLines(reader, new LineHandler() { + @Override + public void handle(String line) { + collection.add(line); + } + }); + 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); + + // 从返回的内容中读取所需内容 + final BufferedReader bReader = getReader(reader); + String line = null; + try { + while ((line = bReader.readLine()) != null) { + lineHandler.handle(line); + } + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + // -------------------------------------------------------------------------------------- read end + + /** + * String 转为流 + * + * @param content 内容 + * @param charsetName 编码 + * @return 字节流 + */ + 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); + } + + /** + * String 转为流 + * + * @param content 内容bytes + * @return 字节流 + * @since 4.1.8 + */ + public static ByteArrayInputStream toStream(byte[] content) { + if (content == null) { + return null; + } + return new ByteArrayInputStream(content); + } + + /** + * 文件转为流 + * + * @param file 文件 + * @return {@link FileInputStream} + */ + public static FileInputStream toStream(File file) { + try { + return new FileInputStream(file); + } catch (FileNotFoundException e) { + throw new IORuntimeException(e); + } + } + + /** + * 转换为{@link BufferedInputStream} + * + * @param in {@link InputStream} + * @return {@link BufferedInputStream} + * @since 4.0.10 + */ + public static BufferedInputStream toBuffered(InputStream in) { + return (in instanceof BufferedInputStream) ? (BufferedInputStream) in : new BufferedInputStream(in); + } + + /** + * 转换为{@link BufferedOutputStream} + * + * @param out {@link OutputStream} + * @return {@link BufferedOutputStream} + * @since 4.0.10 + */ + public static BufferedOutputStream toBuffered(OutputStream out) { + return (out instanceof BufferedOutputStream) ? (BufferedOutputStream) out : new BufferedOutputStream(out); + } + + /** + * 将{@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 (false == 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); + } + + /** + * 将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异常 + */ + 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 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) { + // 静默关闭 + } + } + } + + /** + * 关闭
+ * 关闭失败不会抛出异常 + * + * @param closeable 被关闭的对象 + */ + public static void close(AutoCloseable 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 (false == (input1 instanceof BufferedInputStream)) { + input1 = new BufferedInputStream(input1); + } + if (false == (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 && line2 != null && line1.equals(line2)) { + line1 = br1.readLine(); + line2 = br2.readLine(); + } + return line1 == null ? line2 == null ? true : false : line1.equals(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; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/io/LineHandler.java b/hutool-core/src/main/java/cn/hutool/core/io/LineHandler.java new file mode 100644 index 000000000..361021908 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/LineHandler.java @@ -0,0 +1,14 @@ +package cn.hutool.core.io; + +/** + * 行处理器 + * @author Looly + * + */ +public interface LineHandler { + /** + * 处理一行数据,可以编辑后存入指定地方 + * @param line 行 + */ + void handle(String line); +} diff --git a/hutool-core/src/main/java/cn/hutool/core/io/NullOutputStream.java b/hutool-core/src/main/java/cn/hutool/core/io/NullOutputStream.java new file mode 100644 index 000000000..d6a0a27f5 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/io/StreamProgress.java b/hutool-core/src/main/java/cn/hutool/core/io/StreamProgress.java new file mode 100644 index 000000000..7971698ea --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/StreamProgress.java @@ -0,0 +1,25 @@ +package cn.hutool.core.io; + +/** + * Stream进度条 + * @author Looly + * + */ +public interface StreamProgress { + + /** + * 开始 + */ + public void start(); + + /** + * 进行中 + * @param progressSize 已经进行的大小 + */ + public void progress(long progressSize); + + /** + * 结束 + */ + public void finish(); +} diff --git a/hutool-core/src/main/java/cn/hutool/core/io/checksum/CRC16.java b/hutool-core/src/main/java/cn/hutool/core/io/checksum/CRC16.java new file mode 100644 index 000000000..3fc31c1ad --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/checksum/CRC16.java @@ -0,0 +1,76 @@ +package cn.hutool.core.io.checksum; + +import java.io.Serializable; +import java.util.zip.Checksum; + +/** + * CRC16 循环冗余校验码(Cyclic Redundancy Check)实现
+ * 代码来自:https://github.com/BBSc0der + * + * @author looly + * @since 4.4.1 + */ +public class CRC16 implements Checksum, Serializable { + private static final long serialVersionUID = 1L; + + // CRC16 = bb3d + // Uses irreducible polynomial: 1 + x^2 + x^15 + x^16 + + private static final int[] TABLE = { // + 0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241, // + 0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440, // + 0xcc01, 0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40, // + 0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841, // + 0xd801, 0x18c0, 0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40, // + 0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41, // + 0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0, 0x1680, 0xd641, // + 0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040, // + 0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240, // + 0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441, // + 0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41, // + 0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840, // + 0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41, // + 0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40, // + 0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681, 0x2640, // + 0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041, // + 0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240, // + 0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441, // + 0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41, // + 0xaa01, 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840, // + 0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41, // + 0xbe01, 0x7ec0, 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40, // + 0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640, // + 0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041, // + 0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241, // + 0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481, 0x5440, // + 0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40, // + 0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841, // + 0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40, // + 0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41, // + 0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641, // + 0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081, 0x4040// + }; + + private int sum = 0x0000; + + @Override + public long getValue() { + return sum; + } + + @Override + public void reset() { + sum = 0x0000; + } + + @Override + public void update(byte[] b, int off, int len) { + for (int i = off; i < off + len; i++) + update((int) b[i]); + } + + @Override + public void update(int b) { + sum = (sum >> 8) ^ TABLE[((sum) ^ (b & 0xff)) & 0xff]; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/io/checksum/CRC8.java b/hutool-core/src/main/java/cn/hutool/core/io/checksum/CRC8.java new file mode 100644 index 000000000..e08ad4f82 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/io/checksum/package-info.java b/hutool-core/src/main/java/cn/hutool/core/io/checksum/package-info.java new file mode 100644 index 000000000..5c6b291b2 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/io/file/FileAppender.java b/hutool-core/src/main/java/cn/hutool/core/io/file/FileAppender.java new file mode 100644 index 000000000..51a61a00f --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/file/FileAppender.java @@ -0,0 +1,87 @@ +package cn.hutool.core.io.file; + +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 cn.hutool.core.util.CharsetUtil; + +/** + * 文件追加器
+ * 持有一个文件,在内存中积累一定量的数据后统一追加到文件
+ * 此类只有在写入文件时打开文件,并在写入结束后关闭之。因此此类不需要关闭
+ * 在调用append方法后会缓存于内存,只有超过容量后才会一次性写入文件,因此内存中随时有剩余未写入文件的内容,在最后必须调用flush方法将剩余内容刷入文件 + * + * @author looly + * @since 3.1.2 + */ +public class FileAppender implements Serializable{ + private static final long serialVersionUID = 1L; + + private FileWriter writer; + /** 内存中持有的字符串数 */ + private int capacity; + /** 追加内容是否为新行 */ + private boolean isNewLineMode; + private List list = new ArrayList<>(100); + + /** + * 构造 + * + * @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.capacity = capacity; + this.isNewLineMode = isNewLineMode; + this.writer = FileWriter.create(destFile, charset); + } + + /** + * 追加 + * + * @param line 行 + * @return this + */ + public FileAppender append(String line) { + if (list.size() >= capacity) { + flush(); + } + list.add(line); + return this; + } + + /** + * 刷入到文件 + * + * @return this + */ + public FileAppender flush() { + try(PrintWriter pw = writer.getPrintWriter(true)){ + for (String str : list) { + pw.print(str); + if (isNewLineMode) { + pw.println(); + } + } + } + list.clear(); + return this; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/io/file/FileCopier.java b/hutool-core/src/main/java/cn/hutool/core/io/file/FileCopier.java new file mode 100644 index 000000000..e59108e72 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/file/FileCopier.java @@ -0,0 +1,283 @@ +package cn.hutool.core.io.file; + +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; + +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.StrUtil; + +/** + * 文件拷贝器
+ * 支持以下几种情况: + *

+ * 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 {@link FileCopier} + */ + public static FileCopier create(String srcPath, String destPath) { + return new FileCopier(FileUtil.file(srcPath), FileUtil.file(destPath)); + } + + /** + * 新建一个文件复制器 + * @param src 源文件 + * @param dest 目标文件 + * @return {@link FileCopier} + */ + 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 当拷贝来源是目录时是否只拷贝文件而忽略子目录 + * @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; + final File dest = this.dest; + // check + Assert.notNull(src, "Source File is null !"); + if (false == 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() && false == 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 subDest = isCopyContentIfDir ? dest : FileUtil.mkdir(FileUtil.file(dest, src.getName())); + internalCopyDirContent(src, subDest); + } else {// 复制文件 + 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 && false == copyFilter.accept(src)) { + //被过滤的目录跳过 + return; + } + + if (false == dest.exists()) { + //目标为不存在路径,创建为目录 + dest.mkdirs(); + } else if (false == 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(); + 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 目标文件,如果非覆盖模式必须为目录 + * @throws IORuntimeException IO异常 + */ + private void internalCopyFile(File src, File dest) throws IORuntimeException { + if (null != copyFilter && false == copyFilter.accept(src)) { + //被过滤的文件跳过 + return; + } + + // 如果已经存在目标文件,切为不覆盖模式,跳过之 + if (dest.exists()) { + if(dest.isDirectory()) { + //目标为目录,目录下创建同名文件 + dest = new File(dest, src.getName()); + } + + if(dest.exists() && false == isOverride) { + //非覆盖模式跳过 + return; + } + }else { + //路径不存在则创建父目录 + dest.getParentFile().mkdirs(); + } + + 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[optionList.size()])); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + //----------------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-core/src/main/java/cn/hutool/core/io/file/FileMode.java b/hutool-core/src/main/java/cn/hutool/core/io/file/FileMode.java new file mode 100644 index 000000000..0ac349fa8 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/io/file/FileReader.java b/hutool-core/src/main/java/cn/hutool/core/io/file/FileReader.java new file mode 100644 index 000000000..f6e3540b1 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/file/FileReader.java @@ -0,0 +1,294 @@ +package cn.hutool.core.io.file; + +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; + +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; + +/** + * 文件读取器 + * + * @author Looly + * + */ +public class FileReader extends FileWrapper { + private static final long serialVersionUID = 1L; + + /** + * 创建 FileReader + * @param file 文件 + * @param charset 编码,使用 {@link CharsetUtil} + * @return {@link FileReader} + */ + public static FileReader create(File file, Charset charset){ + return new FileReader(file, charset); + } + + /** + * 创建 FileReader, 编码:{@link FileWrapper#DEFAULT_CHARSET} + * @param file 文件 + * @return {@link 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 = null; + 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 File + * @throws IORuntimeException IO异常 + */ + public File writeToStream(OutputStream out) throws IORuntimeException { + FileInputStream in = null; + try { + in = new FileInputStream(file); + IoUtil.copy(in, out); + }catch (IOException e) { + throw new IORuntimeException(e); + } finally { + IoUtil.close(in); + } + return this.file; + } + + // -------------------------------------------------------------------------- Interface start + /** + * Reader处理接口 + * + * @author Luxiaolei + * + * @param Reader处理返回结果类型 + */ + public interface ReaderHandler { + public T handle(BufferedReader reader) throws IOException; + } + // -------------------------------------------------------------------------- Interface end + + /** + * 检查文件 + * + * @throws IORuntimeException IO异常 + */ + private void checkFile() throws IORuntimeException { + if (false == file.exists()) { + throw new IORuntimeException("File not exist: " + file); + } + if (false == file.isFile()) { + throw new IORuntimeException("Not a file:" + file); + } + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/io/file/FileWrapper.java b/hutool-core/src/main/java/cn/hutool/core/io/file/FileWrapper.java new file mode 100644 index 000000000..59db9c3d1 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/io/file/FileWriter.java b/hutool-core/src/main/java/cn/hutool/core/io/file/FileWriter.java new file mode 100644 index 000000000..12438f713 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/file/FileWriter.java @@ -0,0 +1,391 @@ +package cn.hutool.core.io.file; + +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.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.charset.Charset; +import java.util.Collection; +import java.util.Map; +import java.util.Map.Entry; + +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; + +/** + * 文件写入器 + * @author Looly + * + */ +public class FileWriter extends FileWrapper{ + private static final long serialVersionUID = 1L; + + /** + * 创建 FileWriter + * @param file 文件 + * @param charset 编码,使用 {@link CharsetUtil} + * @return {@link FileWriter} + */ + public static FileWriter create(File file, Charset charset){ + return new FileWriter(file, charset); + } + + /** + * 创建 FileWriter, 编码:{@link FileWrapper#DEFAULT_CHARSET} + * @param file 文件 + * @return {@link 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(Collection list) throws IORuntimeException { + return writeLines(list, false); + } + + /** + * 将列表写入文件,追加模式 + * + * @param 集合元素类型 + * @param list 列表 + * @return 目标文件 + * @throws IORuntimeException IO异常 + */ + public File appendLines(Collection list) throws IORuntimeException { + return writeLines(list, true); + } + + /** + * 将列表写入文件 + * + * @param 集合元素类型 + * @param list 列表 + * @param isAppend 是否追加 + * @return 目标文件 + * @throws IORuntimeException IO异常 + */ + public File writeLines(Collection 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(Collection list, LineSeparator lineSeparator, boolean isAppend) throws IORuntimeException { + try (PrintWriter writer = getPrintWriter(isAppend)){ + for (T t : list) { + if (null != t) { + writer.print(t.toString()); + printNewLine(writer, lineSeparator); + 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 { + FileOutputStream out = null; + try { + out = new FileOutputStream(FileUtil.touch(file), isAppend); + out.write(data, off, len); + out.flush(); + }catch(IOException e){ + throw new IORuntimeException(e); + } finally { + IoUtil.close(out); + } + return file; + } + + /** + * 将流的内容写入文件
+ * 此方法不会关闭输入流 + * + * @param in 输入流,不关闭 + * @return dest + * @throws IORuntimeException IO异常 + */ + public File writeFromStream(InputStream in) throws IORuntimeException { + FileOutputStream out = null; + try { + out = new FileOutputStream(FileUtil.touch(file)); + IoUtil.copy(in, out); + }catch (IOException e) { + throw new IORuntimeException(e); + } finally { + IoUtil.close(out); + } + return file; + } + + /** + * 获得一个输出流对象 + * + * @return 输出流对象 + * @throws IORuntimeException IO异常 + */ + public BufferedOutputStream getOutputStream() throws IORuntimeException { + try { + return new BufferedOutputStream(new FileOutputStream(FileUtil.touch(file))); + } 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() && false == 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/hutool-core/src/main/java/cn/hutool/core/io/file/LineReadWatcher.java b/hutool-core/src/main/java/cn/hutool/core/io/file/LineReadWatcher.java new file mode 100644 index 000000000..18cc739b7 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/file/LineReadWatcher.java @@ -0,0 +1,71 @@ +package cn.hutool.core.io.file; + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.nio.file.WatchEvent; + +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; + +/** + * 行处理的Watcher实现 + * + * @author looly + * @since 4.5.2 + */ +public class LineReadWatcher extends SimpleWatcher implements Runnable { + + private RandomAccessFile randomAccessFile; + private Charset charset; + private 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 (0 == currentLength || position == currentLength) { + // 内容长度不变时忽略此次事件 + return; + } else if (currentLength < position) { + // 如果内容变短,说明文件做了删改,回到内容末尾 + randomAccessFile.seek(currentLength); + return; + } + + // 读取行 + FileUtil.readLines(randomAccessFile, charset, lineHandler); + + // 记录当前读到的位置 + randomAccessFile.seek(currentLength); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/io/file/LineSeparator.java b/hutool-core/src/main/java/cn/hutool/core/io/file/LineSeparator.java new file mode 100644 index 000000000..b5cda7c5c --- /dev/null +++ b/hutool-core/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 String value; + + private LineSeparator(String lineSeparator) { + this.value = lineSeparator; + } + + public String getValue() { + return this.value; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/io/file/Tailer.java b/hutool-core/src/main/java/cn/hutool/core/io/file/Tailer.java new file mode 100644 index 000000000..ef82e5791 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/file/Tailer.java @@ -0,0 +1,223 @@ +package cn.hutool.core.io.file; + +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; + +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.LineHandler; +import cn.hutool.core.lang.Console; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.CharsetUtil; + +/** + * 文件内容跟随器,实现类似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 Charset charset; + /** 行处理器 */ + private LineHandler lineHandler; + /** 初始读取的行数 */ + private int initReadLine; + /** 定时任务检查间隔时长 */ + private long period; + + private RandomAccessFile randomAccessFile; + private 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 (false == async) { + try { + scheduledFuture.get(); + } catch (ExecutionException e) { + throw new UtilException(e); + } catch (InterruptedException e) { + // ignore and exist + } + } + } + + // ---------------------------------------------------------------------------------------- Private method start + /** + * 预读取行 + * + * @throws IOException + */ + 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; + 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 (false == 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 (false == file.exists()) { + throw new UtilException("File [{}] not exist !", file.getAbsolutePath()); + } + if (false == 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/hutool-core/src/main/java/cn/hutool/core/io/file/package-info.java b/hutool-core/src/main/java/cn/hutool/core/io/file/package-info.java new file mode 100644 index 000000000..41e854d49 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/io/package-info.java b/hutool-core/src/main/java/cn/hutool/core/io/package-info.java new file mode 100644 index 000000000..4e046be11 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/io/resource/BytesResource.java b/hutool-core/src/main/java/cn/hutool/core/io/resource/BytesResource.java new file mode 100644 index 000000000..0cba52088 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/resource/BytesResource.java @@ -0,0 +1,83 @@ +package cn.hutool.core.io.resource; + +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; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 基于byte[]的资源获取器
+ * 注意:此对象中getUrl方法始终返回null + * + * @author looly + * @since 4.0.9 + */ +public class BytesResource implements Resource, Serializable { + private static final long serialVersionUID = 1L; + + private byte[] bytes; + private 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 BufferedReader getReader(Charset charset) { + return new BufferedReader(new StringReader(readStr(charset))); + } + + @Override + public String readStr(Charset charset) throws IORuntimeException { + return StrUtil.str(this.bytes, charset); + } + + @Override + public String readUtf8Str() throws IORuntimeException { + return readStr(CharsetUtil.CHARSET_UTF_8); + } + + @Override + public byte[] readBytes() throws IORuntimeException { + return this.bytes; + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/io/resource/ClassPathResource.java b/hutool-core/src/main/java/cn/hutool/core/io/resource/ClassPathResource.java new file mode 100644 index 000000000..744ada561 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/resource/ClassPathResource.java @@ -0,0 +1,145 @@ +package cn.hutool.core.io.resource; + +import java.net.URL; + +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; + +/** + * ClassPath单一资源访问类
+ * 传入路径path必须为相对路径,如果传入绝对路径,Linux路径会去掉开头的“/”,而Windows路径会直接报错。
+ * 传入的path所指向的资源必须存在,否则报错 + * + * @author Looly + * + */ +public class ClassPathResource extends UrlResource { + private static final long serialVersionUID = 1L; + + private String path; + private ClassLoader classLoader; + private 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/hutool-core/src/main/java/cn/hutool/core/io/resource/FileResource.java b/hutool-core/src/main/java/cn/hutool/core/io/resource/FileResource.java new file mode 100644 index 000000000..0d854b185 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/resource/FileResource.java @@ -0,0 +1,59 @@ +package cn.hutool.core.io.resource; + +import java.io.File; +import java.nio.file.Path; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; + +/** + * 文件资源访问对象 + * + * @author looly + * + */ +public class FileResource extends UrlResource { + private static final long serialVersionUID = 1L; + + // ----------------------------------------------------------------------- Constructor start + /** + * 构造 + * + * @param path 文件 + * @since 4.4.1 + */ + public FileResource(Path path) { + this(path.toFile()); + } + + /** + * 构造 + * + * @param file 文件 + */ + public FileResource(File file) { + this(file, file.getName()); + } + + /** + * 构造 + * + * @param file 文件 + * @param fileName 文件名,如果为null获取文件本身的文件名 + */ + public FileResource(File file, String fileName) { + super(URLUtil.getURL(file), StrUtil.isBlank(fileName) ? file.getName() : fileName); + } + + /** + * 构造 + * + * @param path 文件绝对路径或相对ClassPath路径,但是这个路径不能指向一个jar包中的文件 + */ + public FileResource(String path) { + this(FileUtil.file(path)); + } + // ----------------------------------------------------------------------- Constructor end + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/io/resource/InputStreamResource.java b/hutool-core/src/main/java/cn/hutool/core/io/resource/InputStreamResource.java new file mode 100644 index 000000000..0955db2ce --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/resource/InputStreamResource.java @@ -0,0 +1,93 @@ +package cn.hutool.core.io.resource; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.Serializable; +import java.net.URL; +import java.nio.charset.Charset; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.CharsetUtil; + +/** + * 基于{@link InputStream}的资源获取器
+ * 注意:此对象中getUrl方法始终返回null + * + * @author looly + * @since 4.0.9 + */ +public class InputStreamResource implements Resource, Serializable { + private static final long serialVersionUID = 1L; + + private InputStream in; + private 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; + } + + @Override + public BufferedReader getReader(Charset charset) { + return IoUtil.getReader(this.in, charset); + } + + @Override + public String readStr(Charset charset) throws IORuntimeException { + BufferedReader reader = null; + try { + reader = getReader(charset); + return IoUtil.read(reader); + } finally { + IoUtil.close(reader); + } + } + + @Override + public String readUtf8Str() throws IORuntimeException { + return readStr(CharsetUtil.CHARSET_UTF_8); + } + + @Override + public byte[] readBytes() throws IORuntimeException { + InputStream in = null; + try { + in = getStream(); + return IoUtil.readBytes(in); + } finally { + IoUtil.close(in); + } + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/io/resource/MultiFileResource.java b/hutool-core/src/main/java/cn/hutool/core/io/resource/MultiFileResource.java new file mode 100644 index 000000000..60eb05109 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/resource/MultiFileResource.java @@ -0,0 +1,66 @@ +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) { + super(); + add(files); + } + + /** + * 构造 + * + * @param files + */ + public MultiFileResource(File... files) { + super(); + 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/hutool-core/src/main/java/cn/hutool/core/io/resource/MultiResource.java b/hutool-core/src/main/java/cn/hutool/core/io/resource/MultiResource.java new file mode 100644 index 000000000..efa737bd7 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/resource/MultiResource.java @@ -0,0 +1,127 @@ +package cn.hutool.core.io.resource; + +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; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.io.IORuntimeException; + +/** + * 多资源组合资源
+ * 此资源为一个利用游标自循环资源,只有调用{@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 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 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 Resource next() { + if (cursor >= resources.size()) { + throw new ConcurrentModificationException(); + } + this.cursor++; + return this; + } + + @Override + public void remove() { + this.resources.remove(this.cursor); + } + + /** + * 重置游标 + */ + public void reset() { + this.cursor = 0; + } + + /** + * 增加资源 + * @param resource 资源 + * @return this + */ + public MultiResource add(Resource resource) { + this.resources.add(resource); + return this; + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/io/resource/NoResourceException.java b/hutool-core/src/main/java/cn/hutool/core/io/resource/NoResourceException.java new file mode 100644 index 000000000..1053e6dbd --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/resource/NoResourceException.java @@ -0,0 +1,49 @@ +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 是否为指定类型异常 + */ + public boolean causeInstanceOf(Class clazz) { + Throwable cause = this.getCause(); + if (null != cause && clazz.isInstance(cause)) { + return true; + } + return false; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/io/resource/Resource.java b/hutool-core/src/main/java/cn/hutool/core/io/resource/Resource.java new file mode 100644 index 000000000..e2f58ac47 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/resource/Resource.java @@ -0,0 +1,73 @@ +package cn.hutool.core.io.resource; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.charset.Charset; + +import cn.hutool.core.io.IORuntimeException; + +/** + * 资源接口定义
+ * 资源可以是文件、URL、ClassPath中的文件亦或者jar包中的文件 + * + * @author looly + * @since 3.2.1 + */ +public interface Resource { + + /** + * 获取资源名,例如文件资源的资源名为文件名 + * @return 资源名 + * @since 4.0.13 + */ + String getName(); + + /** + * 获得解析后的{@link URL} + * @return 解析后的{@link URL} + */ + URL getUrl(); + + /** + * 获得 {@link InputStream} + * @return {@link InputStream} + */ + InputStream getStream(); + + /** + * 获得Reader + * @param charset 编码 + * @return {@link BufferedReader} + */ + BufferedReader getReader(Charset charset); + + /** + * 读取资源内容,读取完毕后会关闭流
+ * 关闭流并不影响下一次读取 + * + * @param charset 编码 + * @return 读取资源内容 + * @throws IORuntimeException 包装{@link IOException} + */ + String readStr(Charset charset) throws IORuntimeException; + + /** + * 读取资源内容,读取完毕后会关闭流
+ * 关闭流并不影响下一次读取 + * + * @return 读取资源内容 + * @throws IORuntimeException 包装IOException + */ + String readUtf8Str() throws IORuntimeException; + + /** + * 读取资源内容,读取完毕后会关闭流
+ * 关闭流并不影响下一次读取 + * + * @return 读取资源内容 + * @throws IORuntimeException 包装IOException + */ + byte[] readBytes() throws IORuntimeException; +} diff --git a/hutool-core/src/main/java/cn/hutool/core/io/resource/ResourceUtil.java b/hutool-core/src/main/java/cn/hutool/core/io/resource/ResourceUtil.java new file mode 100644 index 000000000..6867c30d7 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/resource/ResourceUtil.java @@ -0,0 +1,190 @@ +package cn.hutool.core.io.resource; + +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; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.collection.EnumerationIter; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.ClassLoaderUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; + +/** + * ClassPath资源工具类 + * + * @author Looly + * + */ +public class ResourceUtil { + + /** + * 读取Classpath下的资源为字符串,使用UTF-8编码 + * + * @param resource 资源路径,使用相对ClassPath的路径 + * @return 资源内容 + * @since 3.1.1 + */ + public static String readUtf8Str(String resource) { + return readStr(resource, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 读取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 resurce ClassPath资源 + * @return {@link InputStream} + * @throws NoResourceException 资源不存在异常 + * @since 3.1.2 + */ + public static InputStream getStream(String resurce) throws NoResourceException { + return getResourceObj(resurce).getStream(); + } + + /** + * 从ClassPath资源中获取{@link InputStream},当资源不存在时返回null + * + * @param resurce ClassPath资源 + * @return {@link InputStream} + * @since 4.0.3 + */ + public static InputStream getStreamSafe(String resurce) { + try { + return getResourceObj(resurce).getStream(); + } catch (NoResourceException e) { + // ignore + } + return null; + } + + /** + * 从ClassPath资源中获取{@link BufferedReader} + * + * @param resurce ClassPath资源 + * @param charset 编码 + * @return {@link InputStream} + * @since 3.1.2 + */ + public static BufferedReader getReader(String resurce, Charset charset) { + return getResourceObj(resurce).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) { + final Enumeration resources; + try { + resources = ClassLoaderUtil.getClassLoader().getResources(resource); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return CollectionUtil.newArrayList(resources); + } + + /** + * 获取指定路径下的资源Iterator
+ * 路径格式必须为目录格式,用/分隔,例如: + * + *
+	 * config/a
+	 * spring/xml
+	 * 
+ * + * @param resource 资源路径 + * @return 资源列表 + * @since 4.1.5 + */ + public static EnumerationIter getResourceIter(String resource) { + final Enumeration resources; + try { + resources = ClassLoaderUtil.getClassLoader().getResources(resource); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return new EnumerationIter<>(resources); + } + + /** + * 获得资源相对路径对应的URL + * + * @param resource 资源相对路径 + * @param baseClass 基准Class,获得的相对路径相对于此Class所在路径,如果为{@code null}则相对ClassPath + * @return {@link URL} + */ + public static URL getResource(String resource, Class baseClass) { + 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/hutool-core/src/main/java/cn/hutool/core/io/resource/StringResource.java b/hutool-core/src/main/java/cn/hutool/core/io/resource/StringResource.java new file mode 100644 index 000000000..53a6d108a --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/resource/StringResource.java @@ -0,0 +1,95 @@ +package cn.hutool.core.io.resource; + +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; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.CharsetUtil; + +/** + * 字符串资源,字符串做为资源 + * + * @author looly + * @since 4.1.0 + */ +public class StringResource implements Resource, Serializable { + private static final long serialVersionUID = 1L; + + private String data; + private String name; + private Charset charset; + + /** + * 构造,使用UTF8编码 + * + * @param data 资源数据 + */ + public StringResource(String data) { + this(data, null); + } + + /** + * 构造,使用UTF8编码 + * + * @param data 资源数据 + * @param name 资源名称 + */ + public StringResource(String data, String name) { + this(data, name, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 构造 + * + * @param data 资源数据 + * @param name 资源名称 + * @param charset 编码 + */ + public StringResource(String data, String name, Charset charset) { + this.data = data; + this.name = name; + this.charset = charset; + } + + @Override + public String getName() { + return 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)); + } + + @Override + public String readStr(Charset charset) throws IORuntimeException { + return this.data; + } + + @Override + public String readUtf8Str() throws IORuntimeException { + return this.data; + } + + @Override + public byte[] readBytes() throws IORuntimeException { + return this.data.getBytes(this.charset); + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/io/resource/UrlResource.java b/hutool-core/src/main/java/cn/hutool/core/io/resource/UrlResource.java new file mode 100644 index 000000000..89b0d6bb6 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/resource/UrlResource.java @@ -0,0 +1,130 @@ +package cn.hutool.core.io.resource; + +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStream; +import java.io.Serializable; +import java.net.URL; +import java.nio.charset.Charset; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.URLUtil; + +/** + * URL资源访问类 + * @author Looly + * + */ +public class UrlResource implements Resource, Serializable{ + private static final long serialVersionUID = 1L; + + protected URL url; + protected String name; + + //-------------------------------------------------------------------------------------- Constructor start + /** + * 构造 + * @param url URL + */ + public UrlResource(URL url) { + this(url, null); + } + + /** + * 构造 + * @param url URL,允许为空 + * @param name 资源名称 + */ + public UrlResource(URL url, String name) { + this.url = url; + 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 [{}] not exist!", this.url); + } + return URLUtil.getStream(url); + } + + /** + * 获得Reader + * @param charset 编码 + * @return {@link BufferedReader} + * @since 3.0.1 + */ + public BufferedReader getReader(Charset charset){ + return URLUtil.getReader(this.url, charset); + } + + //------------------------------------------------------------------------------- read + @Override + public String readStr(Charset charset) throws IORuntimeException{ + BufferedReader reader = null; + try { + reader = getReader(charset); + return IoUtil.read(reader); + } finally { + IoUtil.close(reader); + } + } + + @Override + public String readUtf8Str() throws IORuntimeException{ + return readStr(CharsetUtil.CHARSET_UTF_8); + } + + @Override + public byte[] readBytes() throws IORuntimeException{ + InputStream in = null; + try { + in = getStream(); + return IoUtil.readBytes(in); + } finally { + IoUtil.close(in); + } + } + + /** + * 获得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/hutool-core/src/main/java/cn/hutool/core/io/resource/WebAppResource.java b/hutool-core/src/main/java/cn/hutool/core/io/resource/WebAppResource.java new file mode 100644 index 000000000..d7fea7a79 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/io/resource/package-info.java b/hutool-core/src/main/java/cn/hutool/core/io/resource/package-info.java new file mode 100644 index 000000000..e5281a662 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/io/watch/SimpleWatcher.java b/hutool-core/src/main/java/cn/hutool/core/io/watch/SimpleWatcher.java new file mode 100644 index 000000000..91604afc6 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/io/watch/WatchException.java b/hutool-core/src/main/java/cn/hutool/core/io/watch/WatchException.java new file mode 100644 index 000000000..bcab69d66 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/io/watch/WatchMonitor.java b/hutool-core/src/main/java/cn/hutool/core/io/watch/WatchMonitor.java new file mode 100644 index 000000000..b813d09ca --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/watch/WatchMonitor.java @@ -0,0 +1,473 @@ +package cn.hutool.core.io.watch; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +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.LinkOption; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardWatchEventKinds; +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; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.io.watch.watchers.WatcherChain; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; + +/** + * 路径监听器
+ * 监听器可监听目录或文件
+ * 如果监听的Path不存在,则递归创建空目录然后监听此空目录
+ * 递归监听目录时,并不会监听新创建的目录 + * + * @author Looly + * + */ +public class WatchMonitor extends Thread implements Closeable, Serializable{ + private static final long serialVersionUID = 1L; + + /** 事件丢失 */ + public static final WatchEvent.Kind OVERFLOW = StandardWatchEventKinds.OVERFLOW; + /** 修改事件 */ + public static final WatchEvent.Kind ENTRY_MODIFY = StandardWatchEventKinds.ENTRY_MODIFY; + /** 创建事件 */ + public static final WatchEvent.Kind ENTRY_CREATE = StandardWatchEventKinds.ENTRY_CREATE; + /** 删除事件 */ + public static final WatchEvent.Kind ENTRY_DELETE = StandardWatchEventKinds.ENTRY_DELETE; + /** 全部事件 */ + public static final WatchEvent.Kind[] EVENTS_ALL = {// + StandardWatchEventKinds.OVERFLOW, //事件丢失 + StandardWatchEventKinds.ENTRY_MODIFY, //修改 + StandardWatchEventKinds.ENTRY_CREATE, //创建 + StandardWatchEventKinds.ENTRY_DELETE //删除 + }; + + /** 监听路径,必须为目录 */ + private Path path; + /** 递归目录的最大深度,当小于1时不递归下层目录 */ + private int maxDepth; + /** 监听的文件,对于单文件监听不为空 */ + private Path filePath; + + /** 监听服务 */ + private WatchService watchService; + /** 监听器 */ + private Watcher watcher; + /** 监听事件列表 */ + private WatchEvent.Kind[] events; + + /** 监听是否已经关闭 */ + private boolean isClosed; + + /** WatchKey 和 Path的对应表 */ + private Map watchKeyPathMap = new HashMap<>(); + + //------------------------------------------------------ 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 {@link WatchMonitor} + */ + public static WatchMonitor createAll(URI uri, Watcher watcher){ + return createAll(Paths.get(uri), watcher); + } + + /** + * 创建并初始化监听,监听所有事件 + * @param url URL + * @param watcher {@link Watcher} + * @return {@link 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 {@link WatchMonitor} + */ + public static WatchMonitor createAll(File file, Watcher watcher){ + return createAll(file.toPath(), watcher); + } + + /** + * 创建并初始化监听,监听所有事件 + * @param path 路径 + * @param watcher {@link Watcher} + * @return {@link WatchMonitor} + */ + public static WatchMonitor createAll(String path, Watcher watcher){ + return createAll(Paths.get(path), watcher); + } + + /** + * 创建并初始化监听,监听所有事件 + * @param path 路径 + * @param watcher {@link Watcher} + * @return {@link 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异常时抛出此异常 + */ + public void init() throws WatchException{ + //获取目录或文件路径 + if(false ==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) && false ==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(); + } + + //初始化监听 + try { + watchService = FileSystems.getDefault().newWatchService(); + } catch (IOException e) { + throw new WatchException(e); + } + + isClosed = false; + } + + /** + * 设置监听
+ * 多个监听请使用{@link WatcherChain} + * + * @param watcher 监听 + * @return {@link 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 (false == isClosed) { + WatchKey wk; + try { + wk = watchService.take(); + } catch (InterruptedException | ClosedWatchServiceException e) { +// log.warn(e); + return; + } + + final Path currentPath = watchKeyPathMap.get(wk); + WatchEvent.Kind kind; + for (WatchEvent event : wk.pollEvents()) { + kind = event.kind(); + if(null != this.filePath && false == this.filePath.endsWith(event.context().toString())){ +// log.debug("[{}] is not fit for [{}], pass it.", event.context(), this.filePath.getFileName()); + continue; + } + + if(kind == StandardWatchEventKinds.ENTRY_CREATE){ + watcher.onCreate(event, currentPath); + }else if(kind == StandardWatchEventKinds.ENTRY_MODIFY){ + watcher.onModify(event, currentPath); + }else if(kind == StandardWatchEventKinds.ENTRY_DELETE){ + watcher.onDelete(event, currentPath); + }else if(kind == StandardWatchEventKinds.OVERFLOW){ + watcher.onOverflow(event, currentPath); + } + } + wk.reset(); + } + } + + /** + * 当监听目录时,监听目录的最大深度
+ * 当设置值为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; + } + + /** + * 关闭监听 + */ + @Override + public void close(){ + isClosed = true; + IoUtil.close(watchService); + } + + //------------------------------------------------------ private method start + /** + * 注册监听路径 + */ + private void registerPath() { + registerPath(this.path, (null != this.filePath) ? 0 : this.maxDepth); + } + + /** + * 将指定路径加入到监听中 + * @param path 路径 + * @param maxDepth 递归下层目录的最大深度 + * @return {@link WatchKey} + */ + private void registerPath(Path path, int maxDepth) { + try { + final WatchKey key = path.register(watchService, ArrayUtil.isEmpty(this.events) ? EVENTS_ALL : this.events); + 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) { + //对于禁止访问的目录,跳过监听 + return; + } + throw new WatchException(e); + } + } + //------------------------------------------------------ private method end +} diff --git a/hutool-core/src/main/java/cn/hutool/core/io/watch/WatchUtil.java b/hutool-core/src/main/java/cn/hutool/core/io/watch/WatchUtil.java new file mode 100644 index 000000000..eb38fb6a5 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/watch/WatchUtil.java @@ -0,0 +1,380 @@ +package cn.hutool.core.io.watch; + +import java.io.File; +import java.net.URI; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.WatchEvent; + +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; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/io/watch/Watcher.java b/hutool-core/src/main/java/cn/hutool/core/io/watch/Watcher.java new file mode 100644 index 000000000..d1fb298b1 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/watch/Watcher.java @@ -0,0 +1,39 @@ +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路径 + */ + public void onCreate(WatchEvent event, Path currentPath); + + /** + * 文件修改时执行的方法
+ * 文件修改可能触发多次 + * @param event 事件 + * @param currentPath 事件发生的当前Path路径 + */ + public void onModify(WatchEvent event, Path currentPath); + + /** + * 文件删除时执行的方法 + * @param event 事件 + * @param currentPath 事件发生的当前Path路径 + */ + public void onDelete(WatchEvent event, Path currentPath); + + /** + * 事件丢失或出错时执行的方法 + * @param event 事件 + * @param currentPath 事件发生的当前Path路径 + */ + public void onOverflow(WatchEvent event, Path currentPath); +} diff --git a/hutool-core/src/main/java/cn/hutool/core/io/watch/package-info.java b/hutool-core/src/main/java/cn/hutool/core/io/watch/package-info.java new file mode 100644 index 000000000..be2839ca9 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/io/watch/watchers/DelayWatcher.java b/hutool-core/src/main/java/cn/hutool/core/io/watch/watchers/DelayWatcher.java new file mode 100644 index 000000000..88e839a8f --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/watch/watchers/DelayWatcher.java @@ -0,0 +1,108 @@ +package cn.hutool.core.io.watch.watchers; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.WatchEvent; +import java.nio.file.WatchService; +import java.util.Set; + +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; + +/** + * 延迟观察者
+ * 使用此观察者通过定义一定的延迟时间,解决{@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(new Runnable(){ + @Override + public void run() { + ThreadUtil.sleep(delay); + eventSet.remove(Paths.get(currentPath.toString(), event.context().toString())); + watcher.onModify(event, currentPath); + } + }); + } + //---------------------------------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-core/src/main/java/cn/hutool/core/io/watch/watchers/IgnoreWatcher.java b/hutool-core/src/main/java/cn/hutool/core/io/watch/watchers/IgnoreWatcher.java new file mode 100644 index 000000000..c832c4a6a --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/io/watch/watchers/WatcherChain.java b/hutool-core/src/main/java/cn/hutool/core/io/watch/watchers/WatcherChain.java new file mode 100644 index 000000000..ce1578336 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/io/watch/watchers/WatcherChain.java @@ -0,0 +1,80 @@ +package cn.hutool.core.io.watch.watchers; + +import java.nio.file.Path; +import java.nio.file.WatchEvent; +import java.util.Iterator; +import java.util.List; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.io.watch.Watcher; +import cn.hutool.core.lang.Chain; + +/** + * 观察者链
+ * 用于加入多个观察者 + * + * @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 = CollectionUtil.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); + } + } + + @Override + public Iterator iterator() { + return this.chain.iterator(); + } + + @Override + public WatcherChain addChain(Watcher element) { + this.chain.add(element); + return this; + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/io/watch/watchers/package-info.java b/hutool-core/src/main/java/cn/hutool/core/io/watch/watchers/package-info.java new file mode 100644 index 000000000..066908057 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/lang/Assert.java b/hutool-core/src/main/java/cn/hutool/core/lang/Assert.java new file mode 100644 index 000000000..6b9f828fd --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/Assert.java @@ -0,0 +1,655 @@ +package cn.hutool.core.lang; + +import java.util.Collection; +import java.util.Map; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 断言
+ * 断言某些对象或值是否符合规定,否则抛出异常。经常用于做变量检查 + * + * @author Looly + * + */ +public class Assert { + + /** + * 断言是否为真,如果为 {@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 { + if (false == expression) { + throw new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params)); + } + } + + /** + * 断言是否为真,如果为 {@code false} 抛出 {@code IllegalArgumentException} 异常
+ * + *
+	 * Assert.isTrue(i > 0, "The value must be greater than zero");
+	 * 
+ * + * @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} 抛出 {@code IllegalArgumentException} 异常
+ * + *
+	 * Assert.isFalse(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 isFalse(boolean expression, String errorMsgTemplate, Object... params) throws IllegalArgumentException { + if (expression) { + throw 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} 抛出{@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 { + if (object != null) { + throw 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} 抛出{@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 { + if (object == null) { + throw new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params)); + } + return object; + } + + /** + * 断言对象是否不为{@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 + /** + * 检查给定字符串是否为空,为空抛出 {@link IllegalArgumentException} + * + *
+	 * Assert.notEmpty(name, "Name must not be empty");
+	 * 
+ * + * @param text 被检查字符串 + * @param errorMsgTemplate 错误消息模板,变量使用{}表示 + * @param params 参数 + * @return 非空字符串 + * @see StrUtil#isNotEmpty(CharSequence) + * @throws IllegalArgumentException 被检查字符串为空 + */ + public static String notEmpty(String text, String errorMsgTemplate, Object... params) throws IllegalArgumentException { + if (StrUtil.isEmpty(text)) { + throw new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params)); + } + return text; + } + + /** + * 检查给定字符串是否为空,为空抛出 {@link IllegalArgumentException} + * + *
+	 * Assert.notEmpty(name);
+	 * 
+ * + * @param text 被检查字符串 + * @return 被检查的字符串 + * @see StrUtil#isNotEmpty(CharSequence) + * @throws IllegalArgumentException 被检查字符串为空 + */ + public static String notEmpty(String text) throws IllegalArgumentException { + return notEmpty(text, "[Assertion failed] - this String argument must have length; it must not be null or empty"); + } + + /** + * 检查给定字符串是否为空白(null、空串或只包含空白符),为空抛出 {@link IllegalArgumentException} + * + *
+	 * Assert.notBlank(name, "Name must not be blank");
+	 * 
+ * + * @param text 被检查字符串 + * @param errorMsgTemplate 错误消息模板,变量使用{}表示 + * @param params 参数 + * @return 非空字符串 + * @see StrUtil#isNotBlank(CharSequence) + * @throws IllegalArgumentException 被检查字符串为空白 + */ + public static String notBlank(String text, String errorMsgTemplate, Object... params) throws IllegalArgumentException { + if (StrUtil.isBlank(text)) { + throw new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params)); + } + return text; + } + + /** + * 检查给定字符串是否为空白(null、空串或只包含空白符),为空抛出 {@link IllegalArgumentException} + * + *
+	 * Assert.notBlank(name, "Name must not be blank");
+	 * 
+ * + * @param text 被检查字符串 + * @return 非空字符串 + * @see StrUtil#isNotBlank(CharSequence) + * @throws IllegalArgumentException 被检查字符串为空白 + */ + public static String notBlank(String text) throws IllegalArgumentException { + return notBlank(text, "[Assertion failed] - this String argument must have text; it must not be null, empty, or blank"); + } + + /** + * 断言给定字符串是否不被另一个字符串包含(既是否为子串) + * + *
+	 * Assert.doesNotContain(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 { + if (StrUtil.isNotEmpty(textToSearch) && StrUtil.isNotEmpty(substring) && textToSearch.contains(substring)) { + throw new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params)); + } + return substring; + } + + /** + * 断言给定字符串是否不被另一个字符串包含(既是否为子串) + * + *
+	 * Assert.doesNotContain(name, "rod", "Name must not contain '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, "The array must have elements");
+	 * 
+ * + * @param array 被检查的数组 + * @param errorMsgTemplate 异常时的消息模板 + * @param params 参数列表 + * @return 被检查的数组 + * @throws IllegalArgumentException if the object array is {@code null} or has no elements + */ + public static Object[] notEmpty(Object[] array, String errorMsgTemplate, Object... params) throws IllegalArgumentException { + if (ArrayUtil.isEmpty(array)) { + throw new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params)); + } + return array; + } + + /** + * 断言给定数组是否包含元素,数组必须不为 {@code null} 且至少包含一个元素 + * + *
+	 * Assert.notEmpty(array, "The array must have elements");
+	 * 
+ * + * @param array 被检查的数组 + * @return 被检查的数组 + * @throws IllegalArgumentException if the object array is {@code null} or has no elements + */ + public static Object[] notEmpty(Object[] 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, "The array must have non-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 { + if (ArrayUtil.hasNull(array)) { + throw new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params)); + } + return array; + } + + /** + * 断言给定数组是否不包含{@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, "Collection must have elements");
+	 * 
+ * + * @param 集合元素类型 + * @param collection 被检查的集合 + * @param errorMsgTemplate 异常时的消息模板 + * @param params 参数列表 + * @return 非空集合 + * @throws IllegalArgumentException if the collection is {@code null} or has no elements + */ + public static Collection notEmpty(Collection collection, String errorMsgTemplate, Object... params) throws IllegalArgumentException { + if (CollectionUtil.isEmpty(collection)) { + throw new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params)); + } + return collection; + } + + /** + * 断言给定集合非空 + * + *
+	 * Assert.notEmpty(collection);
+	 * 
+ * + * @param 集合元素类型 + * @param collection 被检查的集合 + * @return 被检查集合 + * @throws IllegalArgumentException if the collection is {@code null} or has no elements + */ + public static Collection notEmpty(Collection 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, "Map must have entries");
+	 * 
+ * + * @param Key类型 + * @param Value类型 + * + * @param map 被检查的Map + * @param errorMsgTemplate 异常时的消息模板 + * @param params 参数列表 + * @return 被检查的Map + * @throws IllegalArgumentException if the map is {@code null} or has no entries + */ + public static Map notEmpty(Map map, String errorMsgTemplate, Object... params) throws IllegalArgumentException { + if (CollectionUtil.isEmpty(map)) { + throw new IllegalArgumentException(StrUtil.format(errorMsgTemplate, params)); + } + return map; + } + + /** + * 断言给定Map非空 + * + *
+	 * Assert.notEmpty(map, "Map must have entries");
+	 * 
+ * + * @param Key类型 + * @param Value类型 + * + * @param map 被检查的Map + * @return 被检查的Map + * @throws IllegalArgumentException if the map is {@code null} or has no entries + */ + public static Map notEmpty(Map 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);
+	 * 
+ * + * @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 (false == 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);
+	 * 
+ * + * @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, "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 (false == 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 value 值 + * @param min 最小值(包含) + * @param max 最大值(包含) + * @return 检查后的长度值 + * @since 4.1.10 + */ + public static int checkBetween(int value, int min, int max) { + if (value < min || value > max) { + throw new IllegalArgumentException(StrUtil.format("Length must be between {} and {}.", min, max)); + } + return value; + } + + /** + * 检查值是否在指定范围内 + * + * @param value 值 + * @param min 最小值(包含) + * @param max 最大值(包含) + * @return 检查后的长度值 + * @since 4.1.10 + */ + public static long checkBetween(long value, long min, long max) { + if (value < min || value > max) { + throw new IllegalArgumentException(StrUtil.format("Length must be between {} and {}.", min, max)); + } + return value; + } + + /** + * 检查值是否在指定范围内 + * + * @param value 值 + * @param min 最小值(包含) + * @param max 最大值(包含) + * @return 检查后的长度值 + * @since 4.1.10 + */ + public static double checkBetween(double value, double min, double max) { + if (value < min || value > max) { + throw new IllegalArgumentException(StrUtil.format("Length must be between {} and {}.", min, max)); + } + return value; + } + + /** + * 检查值是否在指定范围内 + * + * @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("Length must be between {} and {}.", min, max)); + } + return value; + } + + // -------------------------------------------------------------------------------------------------------------------------------------------- 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/hutool-core/src/main/java/cn/hutool/core/lang/Chain.java b/hutool-core/src/main/java/cn/hutool/core/lang/Chain.java new file mode 100644 index 000000000..b97235a80 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/lang/ClassScaner.java b/hutool-core/src/main/java/cn/hutool/core/lang/ClassScaner.java new file mode 100644 index 000000000..b5f0d8b65 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/ClassScaner.java @@ -0,0 +1,334 @@ +package cn.hutool.core.lang; + +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; + +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.CharUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; + +/** + * 类扫描器 + * + * @author looly + * @since 4.1.5 + * + */ +public class ClassScaner implements Serializable{ + private static final long serialVersionUID = 1L; + + /** 包名 */ + private String packageName; + /** 包名,最后跟一个点,表示包名,避免在检查前缀时的歧义 */ + private String packageNameWithDot; + /** 包路径,用于文件中对路径操作 */ + private String packageDirName; + /** 包路径,用于jar中对路径操作,在Linux下与packageDirName一致 */ + private String packagePath; + /** 过滤器 */ + private Filter> classFilter; + /** 编码 */ + private Charset charset; + /** 是否初始化类 */ + private boolean initialize; + + private Set> classes = new HashSet>(); + + /** + * 扫描指定包路径下所有包含指定注解的类 + * + * @param packageName 包路径 + * @param annotationClass 注解类 + * @return 类集合 + */ + public static Set> scanPackageByAnnotation(String packageName, final Class annotationClass) { + return scanPackage(packageName, new Filter>() { + @Override + public boolean accept(Class clazz) { + return clazz.isAnnotationPresent(annotationClass); + } + }); + } + + /** + * 扫描指定包路径下所有指定类或接口的子类或实现类 + * + * @param packageName 包路径 + * @param superClass 父类或接口 + * @return 类集合 + */ + public static Set> scanPackageBySuper(String packageName, final Class superClass) { + return scanPackage(packageName, new Filter>() { + @Override + public boolean accept(Class clazz) { + return superClass.isAssignableFrom(clazz) && !superClass.equals(clazz); + } + }); + } + + /** + * 扫描该包路径下所有class文件 + * + * @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); + } + + /** + * 扫描包路径下满足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 ClassScaner(packageName, classFilter).scan(); + } + + /** + * 构造,默认UTF-8编码 + */ + public ClassScaner() { + this(null); + } + + /** + * 构造,默认UTF-8编码 + * + * @param packageName 包名,所有包传入""或者null + */ + public ClassScaner(String packageName) { + this(packageName, null); + } + + /** + * 构造,默认UTF-8编码 + * + * @param packageName 包名,所有包传入""或者null + * @param classFilter 过滤器,无需传入null + */ + public ClassScaner(String packageName, Filter> classFilter) { + this(packageName, classFilter, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 构造 + * + * @param packageName 包名,所有包传入""或者null + * @param classFilter 过滤器,无需传入null + * @param charset 编码 + */ + public ClassScaner(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; + } + + /** + * 扫描包路径下满足class过滤器条件的所有class文件 + * + * @return 类集合 + */ + public Set> scan() { + for (URL url : ResourceUtil.getResourceIter(this.packagePath)) { + switch (url.getProtocol()) { + case "file": + scanFile(new File(URLUtil.decode(url.getFile(), this.charset.name())), null); + break; + case "jar": + scanJar(URLUtil.getJarFile(url)); + break; + } + } + + if(CollUtil.isEmpty(this.classes)) { + scanJavaClassPaths(); + } + + return Collections.unmodifiableSet(this.classes); + } + + /** + * 设置是否在扫描到类时初始化类 + * + * @param initialize 是否初始化类 + */ + public void setInitialize(boolean initialize) { + this.initialize = initialize; + } + + // --------------------------------------------------------------------------------------------------- Private method start + /** + * 扫描Java指定的ClassPath路径 + * + * @return 扫描到的类 + */ + 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()) { + for (File subFile : file.listFiles()) { + 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 (name.startsWith(this.packagePath)) { + if (name.endsWith(FileUtil.CLASS_EXT) && false == entry.isDirectory()) { + final String className = name// + .substring(0, name.length() - 6)// + .replace(CharUtil.SLASH, CharUtil.DOT);// + addIfAccept(loadClass(className)); + } + } + } + } + + /** + * 加载类 + * + * @param className 类名 + * @return 加载的类 + */ + private Class loadClass(String className) { + Class clazz = null; + try { + clazz = Class.forName(className, this.initialize, ClassUtil.getClassLoader()); + } catch (NoClassDefFoundError e) { + // 由于依赖库导致的类无法加载,直接跳过此类 + } catch (UnsupportedClassVersionError e) { + // 版本导致的不兼容的类,跳过 + } catch (Exception e) { + throw new RuntimeException(e); + // Console.error(e); + } + return clazz; + } + + /** + * 通过过滤器,是否满足接受此类的条件 + * + * @param clazz 类 + * @return 是否接受 + */ + 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(className.startsWith(this.packageNameWithDot)) { + addIfAccept(loadClass(className)); + } + } + } + + /** + * 通过过滤器,是否满足接受此类的条件 + * + * @param clazz 类 + * @return 是否接受 + */ + 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/hutool-core/src/main/java/cn/hutool/core/lang/ConsistentHash.java b/hutool-core/src/main/java/cn/hutool/core/lang/ConsistentHash.java new file mode 100644 index 000000000..e9a9b902e --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/ConsistentHash.java @@ -0,0 +1,113 @@ +package cn.hutool.core.lang; + +import java.io.Serializable; +import java.util.Collection; +import java.util.SortedMap; +import java.util.TreeMap; + +import cn.hutool.core.util.HashUtil; + +/** + * 一致性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算法 */ + HashFunc 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 = new HashFunc() { + + @Override + public Integer hash(Object key) { + //默认使用FNV1hash算法 + return HashUtil.fnvHash(key.toString()); + } + }; + //初始化节点 + for (T node : nodes) { + add(node); + } + } + + /** + * 构造 + * @param hashFunc hash算法对象 + * @param numberOfReplicas 复制的节点个数,增加每个节点的复制节点有利于负载均衡 + * @param nodes 节点对象 + */ + public ConsistentHash(HashFunc 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.hash(node.toString() + i), node); + } + } + + /** + * 移除节点的同时移除相应的虚拟节点 + * @param node 节点对象 + */ + public void remove(T node) { + for (int i = 0; i < numberOfReplicas; i++) { + circle.remove(hashFunc.hash(node.toString() + i)); + } + } + + /** + * 获得一个最近的顺时针节点 + * @param key 为给定键取Hash,取得顺时针方向上最近的一个虚拟节点对应的实际节点 + * @return 节点对象 + */ + public T get(Object key) { + if (circle.isEmpty()) { + return null; + } + int hash = hashFunc.hash(key); + if (!circle.containsKey(hash)) { + SortedMap tailMap = circle.tailMap(hash); //返回此映射的部分视图,其键大于等于 hash + hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey(); + } + //正好命中 + return circle.get(hash); + } + + /** + * Hash算法对象,用于自定义hash算法 + * @author xiaoleilu + * + */ + public interface HashFunc { + public Integer hash(Object key); + } +} \ No newline at end of file diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/Console.java b/hutool-core/src/main/java/cn/hutool/core/lang/Console.java new file mode 100644 index 000000000..d4a01dade --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/Console.java @@ -0,0 +1,182 @@ +package cn.hutool.core.lang; + +import static java.lang.System.out; + +import java.util.Scanner; + +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.StrUtil; + +import static java.lang.System.err; + +/** + * 命令行(控制台)工具方法类
+ * 此类主要针对{@link System#out} 和 {@link System#err} 做封装。 + * + * @author Looly + * + */ + +public class Console { + + // --------------------------------------------------------------------------------- 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) { + Throwable e = (Throwable) obj; + log(e, e.getMessage()); + } else { + log("{}", obj); + } + } + + /** + * 同 System.out.print()方法,打印控制台日志 + * + * @param obj 要打印的对象 + * @since 3.3.1 + */ + public static void print(Object obj) { + print("{}", obj); + } + + /** + * 打印进度条 + * + * @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 值 + */ + public static void log(String template, Object... values) { + log(null, template, values); + } + + /** + * 同 System.out.print()方法,打印控制台日志 + * + * @param template 文本模板,被替换的部分用 {} 表示 + * @param values 值 + * @since 3.3.1 + */ + public static void print(String template, Object... values) { + out.print(StrUtil.format(template, values)); + } + + /** + * 同 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) { + t.printStackTrace(); + out.flush(); + } + } + + // --------------------------------------------------------------------------------- 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("{}", obj); + } + } + + /** + * 同 System.err.println()方法,打印控制台日志 + * + * @param template 文本模板,被替换的部分用 {} 表示 + * @param values 值 + */ + public static void error(String template, Object... values) { + error(null, template, values); + } + + /** + * 同 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(); + } + } + + // --------------------------------------------------------------------------------- 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().next(); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/Dict.java b/hutool-core/src/main/java/cn/hutool/core/lang/Dict.java new file mode 100644 index 000000000..2fbed5924 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/Dict.java @@ -0,0 +1,465 @@ +package cn.hutool.core.lang; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.getter.BasicTypeGetter; + +/** + * 字典对象,扩充了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); + } + // --------------------------------------------------------------- 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.mapToBean(this, clazz, false); + } + + /** + * 填充Value Object对象,忽略大小写 + * + * @param Bean类型 + * @param clazz Value Object(或者POJO)的类 + * @return vo + */ + public T toBeanIgnoreCase(Class clazz) { + return BeanUtil.mapToBeanIgnoreCase(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 = CollectionUtil.newHashSet(withoutNames); + for (Map.Entry entry : dict.entrySet()) { + if (withoutSet.contains(entry.getKey())) { + continue; + } + + final Object value = this.get(entry.getKey()); + if (null != value && value.equals(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 字段名 + * @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 字段值 + */ + public Date getDate(String attr) { + return get(attr, null); + } + + /** + * @param attr 字段名 + * @return 字段值 + */ + public Time getTime(String attr) { + return get(attr, null); + } + + /** + * @param attr 字段名 + * @return 字段值 + */ + public Timestamp getTimestamp(String attr) { + return get(attr, null); + } + + /** + * @param attr 字段名 + * @return 字段值 + */ + public Number getNumber(String attr) { + return get(attr, null); + } + // -------------------------------------------------------------------- Get end + + @Override + public Object put(String key, Object value) { + return super.put(customKey(key), value); + } + + @Override + public Dict clone() { + return (Dict) super.clone(); + } + + /** + * 将Key转为小写 + * + * @param key KEY + * @return 小写KEY + */ + private String customKey(String key) { + if (this.caseInsensitive && null != key) { + key = key.toLowerCase(); + } + return key; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/Editor.java b/hutool-core/src/main/java/cn/hutool/core/lang/Editor.java new file mode 100644 index 000000000..d57110abb --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/Editor.java @@ -0,0 +1,23 @@ +package cn.hutool.core.lang; + +/** + * 编辑器接口,常用于对于集合中的元素做统一编辑
+ * 此编辑器两个作用: + * + *
+ * 1、如果返回值为null,表示此值被抛弃 
+ * 2、对对象做修改
+ * 
+ * + * @param 被编辑对象类型 + * @author Looly + */ +public interface Editor { + /** + * 修改过滤后的结果 + * + * @param t 被过滤的对象 + * @return 修改后的对象,如果被过滤返回null + */ + public T edit(T t); +} diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/Filter.java b/hutool-core/src/main/java/cn/hutool/core/lang/Filter.java new file mode 100644 index 000000000..8a7266236 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/Filter.java @@ -0,0 +1,15 @@ +package cn.hutool.core.lang; + +/** + * 过滤器接口 + * @author Looly + * + */ +public interface Filter { + /** + * 是否接受对象 + * @param t 检查的对象 + * @return 是否接受对象 + */ + boolean accept(T t); +} \ No newline at end of file diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/Holder.java b/hutool-core/src/main/java/cn/hutool/core/lang/Holder.java new file mode 100644 index 000000000..9d225ef2d --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/Holder.java @@ -0,0 +1,44 @@ +package cn.hutool.core.lang; + +import cn.hutool.core.lang.mutable.MutableObj; + +/** + * 为不可变的对象引用提供一个可变的包装,在java中支持引用传递。 + * @author Looly + * + * @param 所持有值类型 + */ +public final class Holder extends MutableObj{ + private static final long serialVersionUID = -3119568580130118011L; + + /** + * 新建Holder类,持有指定值,当值为空时抛出空指针异常 + * + * @param 被持有的对象类型 + * @param value 值,不能为空 + * @return Holder + */ + public static Holder of(T value) throws NullPointerException{ + if(null == value){ + throw new NullPointerException("Holder can not hold a null value!"); + } + return new Holder<>(value); + } + + //--------------------------------------------------------------------------- Constructor start + /** + * 构造 + */ + public Holder() { + super(); + } + + /** + * 构造 + * @param value 被包装的对象 + */ + public Holder(T value) { + super(value); + } + //--------------------------------------------------------------------------- Constructor end +} diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/JarClassLoader.java b/hutool-core/src/main/java/cn/hutool/core/lang/JarClassLoader.java new file mode 100644 index 000000000..8e351c6aa --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/JarClassLoader.java @@ -0,0 +1,166 @@ +package cn.hutool.core.lang; + +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.List; + +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; + +/** + * 外部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, new Object[] { 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()); + } + // ------------------------------------------------------------------- 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 目录 + * @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, new FileFilter() { + @Override + public boolean accept(File file) { + return isJarFile(file); + } + }); + } + + /** + * 是否为jar文件 + * + * @param file 文件 + * @return 是否为jar文件 + * @since 4.4.2 + */ + private static boolean isJarFile(File file) { + if (false == FileUtil.isFile(file)) { + return false; + } + return file.getPath().toLowerCase().endsWith(".jar"); + } + // ------------------------------------------------------------------- Private method end +} diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/Matcher.java b/hutool-core/src/main/java/cn/hutool/core/lang/Matcher.java new file mode 100644 index 000000000..3af898c45 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/Matcher.java @@ -0,0 +1,16 @@ +package cn.hutool.core.lang; + +/** + * 匹配接口 + * @author Looly + * + * @param 匹配的对象类型 + */ +public interface Matcher{ + /** + * 给定对象是否匹配 + * @param t 对象 + * @return 是否匹配 + */ + public boolean match(T t); +} diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/MurmurHash.java b/hutool-core/src/main/java/cn/hutool/core/lang/MurmurHash.java new file mode 100644 index 000000000..5caaa99bc --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/MurmurHash.java @@ -0,0 +1,351 @@ +package cn.hutool.core.lang; + +import java.io.Serializable; +import java.nio.charset.Charset; + +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 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; + + /** + * 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) { + int hash = seed; + final int nblocks = length >> 2; + + // body + for (int i = 0; i < nblocks; i++) { + int i_4 = i << 2; + int k = (data[i_4] & 0xff) // + | ((data[i_4 + 1] & 0xff) << 8) // + | ((data[i_4 + 2] & 0xff) << 16) // + | ((data[i_4 + 3] & 0xff) << 24); + + // mix functions + k *= C1_32; + k = Integer.rotateLeft(k, R1_32); + k *= C2_32; + hash ^= k; + hash = Integer.rotateLeft(hash, R2_32) * M_32 + N_32; + } + + // tail + int idx = nblocks << 2; + int k1 = 0; + switch (length - idx) { + case 3: + k1 ^= data[idx + 2] << 16; + case 2: + k1 ^= data[idx + 1] << 8; + case 1: + k1 ^= data[idx]; + + // mix functions + k1 *= C1_32; + k1 = Integer.rotateLeft(k1, R1_32); + k1 *= C2_32; + hash ^= k1; + } + + // finalization + hash ^= length; + hash ^= (hash >>> 16); + hash *= 0x85ebca6b; + hash ^= (hash >>> 13); + hash *= 0xc2b2ae35; + hash ^= (hash >>> 16); + + return 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 = ((long) data[i8] & 0xff) // + | (((long) data[i8 + 1] & 0xff) << 8) // + | (((long) data[i8 + 2] & 0xff) << 16) // + | (((long) data[i8 + 3] & 0xff) << 24) // + | (((long) data[i8 + 4] & 0xff) << 32)// + | (((long) data[i8 + 5] & 0xff) << 40) // + | (((long) data[i8 + 6] & 0xff) << 48) // + | (((long) data[i8 + 7] & 0xff) << 56); + + // 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) { + long h1 = seed; + long h2 = seed; + final int nblocks = length >> 4; + + // body + for (int i = 0; i < nblocks; i++) { + final int i16 = i << 4; + long k1 = ((long) data[i16] & 0xff) // + | (((long) data[i16 + 1] & 0xff) << 8) // + | (((long) data[i16 + 2] & 0xff) << 16) // + | (((long) data[i16 + 3] & 0xff) << 24) // + | (((long) data[i16 + 4] & 0xff) << 32) // + | (((long) data[i16 + 5] & 0xff) << 40) // + | (((long) data[i16 + 6] & 0xff) << 48) // + | (((long) data[i16 + 7] & 0xff) << 56); + + long k2 = ((long) data[i16 + 8] & 0xff) // + | (((long) data[i16 + 9] & 0xff) << 8) // + | (((long) data[i16 + 10] & 0xff) << 16) // + | (((long) data[i16 + 11] & 0xff) << 24) // + | (((long) data[i16 + 12] & 0xff) << 32) // + | (((long) data[i16 + 13] & 0xff) << 40) // + | (((long) data[i16 + 14] & 0xff) << 48) // + | (((long) data[i16 + 15] & 0xff) << 56); + + // 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; + int tailStart = nblocks << 4; + switch (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 ^= (long) (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 ^= (long) (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 long fmix64(long h) { + h ^= (h >>> 33); + h *= 0xff51afd7ed558ccdL; + h ^= (h >>> 33); + h *= 0xc4ceb9fe1a85ec53L; + h ^= (h >>> 33); + return h; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/ObjectId.java b/hutool-core/src/main/java/cn/hutool/core/lang/ObjectId.java new file mode 100644 index 000000000..d5a22bb3a --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/ObjectId.java @@ -0,0 +1,185 @@ +package cn.hutool.core.lang; + +import java.lang.management.ManagementFactory; +import java.net.NetworkInterface; +import java.nio.ByteBuffer; +import java.util.Enumeration; +import java.util.concurrent.atomic.AtomicInteger; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ClassLoaderUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; + +/** + * MongoDB ID生成策略实现
+ * ObjectId由以下几部分组成: + * + *
+ * 1. Time 时间戳。
+ * 2. Machine 所在主机的唯一标识符,一般是机器主机名的散列值。
+ * 3. PID 进程ID。确保同一机器中不冲突
+ * 4. INC 自增计数器。确保同一秒内产生objectId的唯一性。
+ * 
+ * + * 参考:http://blog.csdn.net/qxc1281/article/details/54021882 + * + * @author looly + * @since 4.0.0 + * + */ +public class ObjectId { + + /** 线程安全的下一个随机数,每次生成自增+1 */ + private static final AtomicInteger nextInc = new AtomicInteger(RandomUtil.randomInt()); + /** 机器信息 */ + private static final int machine = getMachinePiece() | getProcessPiece(); + + /** + * 给定的字符串是否为有效的ObjectId + * + * @param s 字符串 + * @return 是否为有效的ObjectId + */ + public static boolean isValid(String s) { + if (s == null) { + return false; + } + s = StrUtil.removeAll(s, "-"); + final int len = s.length(); + if (len != 24) { + return false; + } + + char c; + for (int i = 0; i < len; i++) { + c = s.charAt(i); + if (c >= '0' && c <= '9') { + continue; + } + if (c >= 'a' && c <= 'f') { + continue; + } + if (c >= 'A' && c <= 'F') { + continue; + } + return false; + } + return true; + } + + /** + * 获取一个objectId的bytes表现形式 + * + * @return objectId + * @since 4.1.15 + */ + public static byte[] nextBytes() { + final ByteBuffer bb = ByteBuffer.wrap(new byte[12]); + bb.putInt((int) DateUtil.currentSeconds());// 4位 + bb.putInt(machine);// 4位 + bb.putInt(nextInc.getAndIncrement());// 4位 + + return bb.array(); + } + + /** + * 获取一个objectId用下划线分割 + * + * @return objectId + */ + public static String next() { + return next(false); + } + + /** + * 获取一个objectId + * + * @param withHyphen 是否包含分隔符 + * @return objectId + */ + public static String next(boolean withHyphen) { + byte[] array = nextBytes(); + final StringBuilder buf = new StringBuilder(withHyphen ? 26 : 24); + int t; + for (int i = 0; i < array.length; i++) { + if (withHyphen && i % 4 == 0 && i != 0) { + buf.append("-"); + } + t = array[i] & 0xff; + if (t < 16) { + buf.append('0'); + } + buf.append(Integer.toHexString(t)); + + } + return buf.toString(); + } + + // ----------------------------------------------------------------------------------------- Private method start + /** + * 获取机器码片段 + * + * @return 机器码片段 + */ + private static int getMachinePiece() { + // 机器码 + int machinePiece; + try { + StringBuilder netSb = new StringBuilder(); + // 返回机器所有的网络接口 + Enumeration e = NetworkInterface.getNetworkInterfaces(); + // 遍历网络接口 + while (e.hasMoreElements()) { + NetworkInterface ni = e.nextElement(); + // 网络接口信息 + netSb.append(ni.toString()); + } + // 保留后两位 + machinePiece = netSb.toString().hashCode() << 16; + } catch (Throwable e) { + // 出问题随机生成,保留后两位 + machinePiece = (RandomUtil.randomInt()) << 16; + } + return machinePiece; + } + + /** + * 获取进程码片段 + * + * @return 进程码片段 + */ + private static int getProcessPiece() { + // 进程码 + // 因为静态变量类加载可能相同,所以要获取进程ID + 加载对象的ID值 + final int processPiece; + // 进程ID初始化 + int processId; + try { + // 获取进程ID + final String processName =ManagementFactory.getRuntimeMXBean().getName(); + final int atIndex = processName.indexOf('@'); + if (atIndex > 0) { + processId = Integer.parseInt(processName.substring(0, atIndex)); + } else { + processId = processName.hashCode(); + } + } catch (Throwable t) { + processId = RandomUtil.randomInt(); + } + + final ClassLoader loader = ClassLoaderUtil.getClassLoader(); + // 返回对象哈希码,无论是否重写hashCode方法 + int loaderId = (loader != null) ? System.identityHashCode(loader) : 0; + + // 进程ID + 对象加载ID + StringBuilder processSb = new StringBuilder(); + processSb.append(Integer.toHexString(processId)); + processSb.append(Integer.toHexString(loaderId)); + // 保留前2位 + processPiece = processSb.toString().hashCode() & 0xFFFF; + + return processPiece; + } + // ----------------------------------------------------------------------------------------- Private method end +} \ No newline at end of file diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/Pair.java b/hutool-core/src/main/java/cn/hutool/core/lang/Pair.java new file mode 100644 index 000000000..f1d6d92ba --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/Pair.java @@ -0,0 +1,53 @@ +package cn.hutool.core.lang; + +import java.io.Serializable; + +import cn.hutool.core.clone.CloneSupport; + +/** + * 键值对对象,只能在构造时传入键值 + * + * @author looly + * + * @param 键类型 + * @param 值类型 + * @since 4.1.5 + */ +public class Pair extends CloneSupport> implements Serializable{ + private static final long serialVersionUID = 1L; + + private K key; + private V 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 + "]"; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/ParameterizedTypeImpl.java b/hutool-core/src/main/java/cn/hutool/core/lang/ParameterizedTypeImpl.java new file mode 100644 index 000000000..07d746132 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/ParameterizedTypeImpl.java @@ -0,0 +1,103 @@ +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; + final Type[] typeArguments = this.actualTypeArguments; + if (useOwner == null) { + buf.append(raw.getName()); + } else { + if (useOwner instanceof Class) { + buf.append(((Class) useOwner).getName()); + } else { + buf.append(useOwner.toString()); + } + buf.append('.').append(raw.getSimpleName()); + } + + appendAllTo(buf.append('<'), ", ", typeArguments).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/hutool-core/src/main/java/cn/hutool/core/lang/PatternPool.java b/hutool-core/src/main/java/cn/hutool/core/lang/PatternPool.java new file mode 100644 index 000000000..507325738 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/PatternPool.java @@ -0,0 +1,170 @@ +package cn.hutool.core.lang; + +import java.util.regex.Pattern; + +import cn.hutool.core.util.ReUtil; + +/** + * 常用正则表达式集合 + * + * @author Looly + * + */ +public class PatternPool { + + /** 英文字母 、数字和下划线 */ + public final static Pattern GENERAL = Pattern.compile("^\\w+$"); + /** 数字 */ + public final static Pattern NUMBERS = Pattern.compile("\\d+"); + /** 字母 */ + public final static Pattern WORD = Pattern.compile("[a-zA-Z]+"); + /** 单个中文汉字 */ + public final static Pattern CHINESE = Pattern.compile(ReUtil.RE_CHINESE); + /** 中文汉字 */ + public final static Pattern CHINESES = Pattern.compile(ReUtil.RE_CHINESES); + /** 分组 */ + public final static Pattern GROUP_VAR = Pattern.compile("\\$(\\d+)"); + /** IP v4 */ + public final static Pattern IPV4 = Pattern.compile("\\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"); + /** IP v6 */ + public final static Pattern IPV6 = Pattern.compile("(([0-9a-fA-F]{1,4}:){7,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]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))"); + /** 货币 */ + public final static Pattern MONEY = Pattern.compile("^(\\d+(?:\\.\\d+)?)$"); + /** 邮件,符合RFC 5322规范,正则来自:http://emailregex.com/ */ + // public final static Pattern EMAIL = Pattern.compile("(\\w|.)+@\\w+(\\.\\w+){1,2}"); + public final static Pattern EMAIL = Pattern.compile("(?:[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])+)\\])", Pattern.CASE_INSENSITIVE); + /** 移动电话 */ + public final static Pattern MOBILE = Pattern.compile("(?:0|86|\\+86)?1[3456789]\\d{9}"); + /** 18位身份证号码 */ + public final static Pattern CITIZEN_ID = Pattern.compile("[1-9]\\d{5}[1-2]\\d{3}((0\\d)|(1[0-2]))(([0|1|2]\\d)|3[0-1])\\d{3}(\\d|X|x)"); + /** 邮编 */ + public final static Pattern ZIP_CODE = Pattern.compile("[1-9]\\d{5}(?!\\d)"); + /** 生日 */ + public final static Pattern BIRTHDAY = Pattern.compile("^(\\d{2,4})([/\\-\\.年]?)(\\d{1,2})([/\\-\\.月]?)(\\d{1,2})日?$"); + /** URL */ + public final static Pattern URL = Pattern.compile("[a-zA-z]+://[^\\s]*"); + /** Http URL */ + public final static Pattern URL_HTTP = Pattern.compile("(https://|http://)?([\\w-]+\\.)+[\\w-]+(:\\d+)*(/[\\w- ./?%&=]*)?"); + /** 中文字、英文字母、数字和下划线 */ + public final static Pattern GENERAL_WITH_CHINESE = Pattern.compile("^[\u4E00-\u9FFF\\w]+$"); + /** UUID */ + public final static Pattern UUID = Pattern.compile("^[0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12}$"); + /** 不带横线的UUID */ + public final static Pattern UUID_SIMPLE = Pattern.compile("^[0-9a-z]{32}$"); + /** 中国车牌号码 */ + public final static Pattern PLATE_NUMBER = Pattern.compile("^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-Z0-9]{4}[A-Z0-9挂学警港澳]{1}$"); + /** MAC地址正则 */ + public static final Pattern MAC_ADDRESS = Pattern.compile("((?:[A-F0-9]{1,2}[:-]){5}[A-F0-9]{1,2})|(?:0x)(\\d{12})(?:.+ETHER)", Pattern.CASE_INSENSITIVE); + /** 16进制字符串 */ + public static final Pattern HEX = Pattern.compile("^[a-f0-9]+$", Pattern.CASE_INSENSITIVE); + /** 时间正则 */ + public static final Pattern TIME = Pattern.compile("\\d{1,2}:\\d{1,2}(:\\d{1,2})?"); + + // ------------------------------------------------------------------------------------------------------------------------------------------------------------------- + /** Pattern池 */ + private static final SimpleCache POOL = new SimpleCache<>(); + + /** + * 先从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); + + Pattern pattern = POOL.get(regexWithFlag); + if (null == pattern) { + pattern = Pattern.compile(regex, flags); + POOL.put(regexWithFlag, pattern); + } + return pattern; + } + + /** + * 移除缓存 + * + * @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 String regex; + private 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) { + if (other.regex != null) { + return false; + } + } else if (!regex.equals(other.regex)) { + return false; + } + return true; + } + + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/Range.java b/hutool-core/src/main/java/cn/hutool/core/lang/Range.java new file mode 100644 index 000000000..cddea6d00 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/Range.java @@ -0,0 +1,212 @@ +package cn.hutool.core.lang; + +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; + +import cn.hutool.core.thread.lock.NoLock; + +/** + * 范围生成器。根据给定的初始值、结束值和步进生成一个步进列表生成器
+ * 由于用户自行实现{@link Steper}来定义步进,因此Range本身无法判定边界(是否达到end),需在step实现边界判定逻辑。 + * + *

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

+ * + * @author Looly + * + * @param 生成范围对象的类型 + */ +public class Range implements Iterable, Iterator, Serializable { + private static final long serialVersionUID = 1L; + + /** 锁保证线程安全 */ + private Lock lock = new ReentrantLock(); + /** 起始对象 */ + private T start; + /** 结束对象 */ + private T end; + /** 当前对象 */ + private T current; + /** 下一个对象 */ + private T next; + /** 步进 */ + private Steper steper; + /** 索引 */ + private int index = 0; + /** 是否包含第一个元素 */ + private boolean includeStart = true; + /** 是否包含最后一个元素 */ + private boolean includeEnd = true; + + /** + * 构造 + * + * @param start 起始对象 + * @param steper 步进 + */ + public Range(T start, Steper steper) { + this(start, null, steper); + } + + /** + * 构造 + * + * @param start 起始对象(包含) + * @param end 结束对象(包含) + * @param steper 步进 + */ + public Range(T start, T end, Steper steper) { + this(start, end, steper, true, true); + } + + /** + * 构造 + * + * @param start 起始对象 + * @param end 结束对象 + * @param steper 步进 + * @param isIncludeStart 是否包含第一个元素 + * @param isIncludeEnd 是否包含最后一个元素 + */ + public Range(T start, T end, Steper steper, boolean isIncludeStart, boolean isIncludeEnd) { + this.start = start; + this.current = start; + this.end = end; + this.steper = steper; + this.next = safeStep(this.current); + 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 (false == includeEnd && this.next.equals(this.end)) { + return false; + } + } finally { + lock.unlock(); + } + return true; + } + + @Override + public T next() { + lock.lock(); + try { + if (false == this.hasNext()) { + throw new NoSuchElementException("Has no next range!"); + } + return nextUncheck(); + } finally { + lock.unlock(); + } + } + + /** + * 获取下一个元素,并将下下个元素准备好 + */ + private T nextUncheck() { + if (0 != this.index || false == this.includeStart) { + // 非第一个元素或不包含第一个元素增加步进 + this.current = this.next; + if (null != this.current) { + this.next = safeStep(this.next); + } + } + index++; + return this.current; + } + + /** + * 不抛异常的获取下一步进的元素,如果获取失败返回{@code null} + * + * @param base 上一个元素 + * @return 下一步进 + */ + private T safeStep(T base) { + T next = null; + try { + next = steper.step(base, this.end, this.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; + } + + /** + * 重置{@link Range} + * + * @return this + */ + public Range reset() { + lock.lock(); + try { + this.current = this.start; + this.index = 0; + } finally { + lock.unlock(); + } + return this; + } + + /** + * 步进接口,此接口用于实现如何对一个对象按照指定步进增加步进
+ * 步进接口可以定义以下逻辑: + * + *
+	 * 1、步进规则,既对象如何做步进
+	 * 2、步进大小,通过实现此接口,在实现类中定义一个对象属性,可灵活定义步进大小
+	 * 3、限制range个数,通过实现此接口,在实现类中定义一个对象属性,可灵活定义limit,限制range个数
+	 * 
+ * + * @author Looly + * + * @param 需要增加步进的对象 + */ + public static interface Steper { + /** + * 增加步进
+ * 增加步进后的返回值如果为{@code null}则表示步进结束
+ * 用户需根据end参数自行定义边界,当达到边界时返回null表示结束,否则Range中边界对象无效,会导致无限循环 + * + * @param current 上一次增加步进后的基础对象 + * @param end 结束对象 + * @param index 当前索引(步进到第几个元素),从0开始计数 + * @return 增加步进后的对象 + */ + T step(T current, T end, int index); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/Replacer.java b/hutool-core/src/main/java/cn/hutool/core/lang/Replacer.java new file mode 100644 index 000000000..2c9e5f4bf --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/Replacer.java @@ -0,0 +1,21 @@ +package cn.hutool.core.lang; + +/** + * 替换器
+ * 通过实现此接口完成指定类型对象的替换操作,替换后的目标类型依旧为指定类型 + * + * @author looly + * + * @param 被替换操作的类型 + * @since 4.1.5 + */ +public interface Replacer { + + /** + * 替换指定类型为目标类型 + * + * @param t 被替换的对象 + * @return 替代后的对象 + */ + public T replace(T t); +} diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/SimpleCache.java b/hutool-core/src/main/java/cn/hutool/core/lang/SimpleCache.java new file mode 100644 index 000000000..cdcdc0efc --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/SimpleCache.java @@ -0,0 +1,118 @@ +package cn.hutool.core.lang; + +import java.io.Serializable; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; +import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; + +import cn.hutool.core.lang.func.Func0; + +/** + * 简单缓存,无超时实现,使用{@link WeakHashMap}实现缓存自动清理 + * @author Looly + * + * @param 键类型 + * @param 值类型 + */ +public class SimpleCache implements Serializable{ + private static final long serialVersionUID = 1L; + + /** 池 */ + private final Map cache = new WeakHashMap<>(); + + private final ReentrantReadWriteLock cacheLock = new ReentrantReadWriteLock(); + private final ReadLock readLock = cacheLock.readLock(); + private final WriteLock writeLock = cacheLock.writeLock(); + + /** + * 从缓存池中查找值 + * + * @param key 键 + * @return 值 + */ + public V get(K key) { + // 尝试读取缓存 + readLock.lock(); + V value; + try { + value = cache.get(key); + } finally { + readLock.unlock(); + } + return value; + } + + /** + * 从缓存中获得对象,当对象不在缓存中或已经过期返回Func0回调产生的对象 + * + * @param key 键 + * @param supplier 如果不存在回调方法,用于生产值对象 + * @return 值对象 + */ + public V get(K key, Func0 supplier) { + V v = get(key); + if (null == v && null != supplier) { + writeLock.lock(); + try { + // 双重检查锁 + v = cache.get(key); + if(null == v) { + try { + v = supplier.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + cache.put(key, v); + } + } finally { + writeLock.unlock(); + } + } + return v; + } + + /** + * 放入缓存 + * @param key 键 + * @param value 值 + * @return 值 + */ + public V put(K key, V value){ + writeLock.lock(); + try { + cache.put(key, value); + } finally { + writeLock.unlock(); + } + return value; + } + + /** + * 移除缓存 + * + * @param key 键 + * @return 移除的值 + */ + public V remove(K key) { + writeLock.lock(); + try { + return cache.remove(key); + } finally { + writeLock.unlock(); + } + } + + /** + * 清空缓存池 + */ + public void clear() { + writeLock.lock(); + try { + this.cache.clear(); + } finally { + writeLock.unlock(); + } + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/Singleton.java b/hutool-core/src/main/java/cn/hutool/core/lang/Singleton.java new file mode 100644 index 000000000..ef9f23b86 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/Singleton.java @@ -0,0 +1,112 @@ +package cn.hutool.core.lang; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 单例类
+ * 提供单例对象的统一管理,当调用get方法时,如果对象池中存在此对象,返回此对象,否则创建新对象返回
+ * + * @author loolly + * + */ +public final class Singleton { + private static Map pool = new ConcurrentHashMap<>(); + + private Singleton() { + } + + /** + * 获得指定类的单例对象
+ * 对象存在于池中返回,否则创建,每次调用此方法获得的对象为同一个对象
+ * 注意:单例针对的是类和对象,因此get方法第一次调用时创建的对象始终唯一,也就是说就算参数变更,返回的依旧是第一次创建的对象 + * + * @param 单例对象类型 + * @param clazz 类 + * @param params 构造方法参数 + * @return 单例对象 + */ + @SuppressWarnings("unchecked") + public static T get(Class clazz, Object... params) { + Assert.notNull(clazz, "Class must be not null !"); + final String key = buildKey(clazz.getName(), params); + T obj = (T) pool.get(key); + + if (null == obj) { + synchronized (Singleton.class) { + obj = (T) pool.get(key); + if (null == obj) { + obj = (T) ReflectUtil.newInstance(clazz, params); + pool.put(key, obj); + } + } + } + + return obj; + } + + /** + * 获得指定类的单例对象
+ * 对象存在于池中返回,否则创建,每次调用此方法获得的对象为同一个对象
+ * + * @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 !"); + pool.put(obj.getClass().getName(), obj); + } + + /** + * 移除指定Singleton对象 + * + * @param clazz 类 + */ + public static void remove(Class clazz) { + if (null != clazz) { + pool.remove(clazz.getName()); + } + } + + /** + * 清除所有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/hutool-core/src/main/java/cn/hutool/core/lang/Snowflake.java b/hutool-core/src/main/java/cn/hutool/core/lang/Snowflake.java new file mode 100644 index 000000000..1be46bb15 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/Snowflake.java @@ -0,0 +1,174 @@ +package cn.hutool.core.lang; + +import java.io.Serializable; + +import cn.hutool.core.date.SystemClock; +import cn.hutool.core.util.StrUtil; + +/** + * Twitter的Snowflake 算法
+ * 分布式系统中,有一些需要使用全局唯一ID的场景,有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。 + * + *

+ * snowflake的结构如下(每部分用-分开):
+ * + *

+ * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
+ * 
+ * + * 第一位为未使用,接下来的41位为毫秒级时间(41位的长度可以使用69年)
+ * 然后是5位datacenterId和5位workerId(10位的长度最多支持部署1024个节点)
+ * 最后12位是毫秒内的计数(12位的计数顺序号支持每个节点每毫秒产生4096个ID序号) + * + * 并且可以通过生成的id反推出生成时间,datacenterId和workerId + *

+ * 参考:http://www.cnblogs.com/relucent/p/4955340.html + * + * @author Looly + * @since 3.0.1 + */ +public class Snowflake implements Serializable{ + private static final long serialVersionUID = 1L; + + // Thu, 04 Nov 2010 01:42:54 GMT + private final long twepoch = 1288834974657L; + private final long workerIdBits = 5L; + private final long datacenterIdBits = 5L; + //// 最大支持机器节点数0~31,一共32个 + private final long maxWorkerId = -1L ^ (-1L << workerIdBits); + // 最大支持数据中心节点数0~31,一共32个 + private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); + // 序列号12位 + private final long sequenceBits = 12L; + // 机器节点左移12位 + private final long workerIdShift = sequenceBits; + // 数据中心节点左移17位 + private final long datacenterIdShift = sequenceBits + workerIdBits; + // 时间毫秒数左移22位 + private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; + private final long sequenceMask = -1L ^ (-1L << sequenceBits);// 4095 + + private long workerId; + private long datacenterId; + private long sequence = 0L; + private long lastTimestamp = -1L; + private boolean useSystemClock; + + /** + * 构造 + * + * @param workerId 终端ID + * @param datacenterId 数据中心ID + */ + public Snowflake(long workerId, long datacenterId) { + this(workerId, datacenterId, false); + } + + /** + * 构造 + * + * @param workerId 终端ID + * @param datacenterId 数据中心ID + * @param isUseSystemClock 是否使用{@link SystemClock} 获取当前时间戳 + */ + public Snowflake(long workerId, long datacenterId, boolean isUseSystemClock) { + if (workerId > maxWorkerId || workerId < 0) { + throw new IllegalArgumentException(StrUtil.format("worker Id can't be greater than {} or less than 0", maxWorkerId)); + } + if (datacenterId > maxDatacenterId || datacenterId < 0) { + throw new IllegalArgumentException(StrUtil.format("datacenter Id can't be greater than {} or less than 0", maxDatacenterId)); + } + this.workerId = workerId; + this.datacenterId = datacenterId; + this.useSystemClock = isUseSystemClock; + } + + /** + * 根据Snowflake的ID,获取机器id + * + * @param id snowflake算法生成的id + * @return 所属机器的id + */ + public long getWorkerId(long id) { + return id >> workerIdShift & ~(-1L << workerIdBits); + } + + /** + * 根据Snowflake的ID,获取数据中心id + * + * @param id snowflake算法生成的id + * @return 所属数据中心 + */ + public long getDataCenterId(long id) { + return id >> datacenterIdShift & ~(-1L << datacenterIdBits); + } + + /** + *根据Snowflake的ID,获取生成时间 + * + * @param id snowflake算法生成的id + * @return 生成的时间 + */ + public long getGenerateDateTime(long id) { + return (id >> timestampLeftShift & ~(-1L << 41L)) + twepoch; + } + + /** + * 下一个ID + * + * @return ID + */ + public synchronized long nextId() { + long timestamp = genTime(); + if (timestamp < lastTimestamp) { + // 如果服务器时间有问题(时钟后退) 报错。 + throw new IllegalStateException(StrUtil.format("Clock moved backwards. Refusing to generate id for {}ms", lastTimestamp - timestamp)); + } + if (lastTimestamp == timestamp) { + sequence = (sequence + 1) & sequenceMask; + if (sequence == 0) { + timestamp = tilNextMillis(lastTimestamp); + } + } else { + sequence = 0L; + } + + lastTimestamp = timestamp; + + return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence; + } + + /** + * 下一个ID(字符串形式) + * + * @return ID 字符串形式 + */ + public String nextIdStr() { + return Long.toString(nextId()); + } + + // ------------------------------------------------------------------------------------------------------------------------------------ Private method start + /** + * 循环等待下一个时间 + * + * @param lastTimestamp 上次记录的时间 + * @return 下一个时间 + */ + private long tilNextMillis(long lastTimestamp) { + long timestamp = genTime(); + while (timestamp <= lastTimestamp) { + timestamp = genTime(); + } + return timestamp; + } + + /** + * 生成时间戳 + * + * @return 时间戳 + */ + private long genTime() { + return this.useSystemClock ? SystemClock.now() : System.currentTimeMillis(); + } + // ------------------------------------------------------------------------------------------------------------------------------------ Private method end +} diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/Tuple.java b/hutool-core/src/main/java/cn/hutool/core/lang/Tuple.java new file mode 100644 index 000000000..83a005f94 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/Tuple.java @@ -0,0 +1,84 @@ +package cn.hutool.core.lang; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Iterator; + +import cn.hutool.core.clone.CloneSupport; +import cn.hutool.core.collection.ArrayIter; + +/** + * 不可变数组类型,用于多值返回
+ * 多值可以支持每个元素值类型不同 + * + * @author Looly + * + */ +public class Tuple extends CloneSupport implements Iterable, Serializable{ + private static final long serialVersionUID = -7689304393482182157L; + + private Object[] members; + + /** + * 构造 + * @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; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.deepHashCode(members); + 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; + if (false == Arrays.deepEquals(members, other.members)) { + return false; + } + return true; + } + + @Override + public String toString() { + return Arrays.toString(members); + } + + @Override + public Iterator iterator() { + return new ArrayIter(members); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/TypeReference.java b/hutool-core/src/main/java/cn/hutool/core/lang/TypeReference.java new file mode 100644 index 000000000..fe7b1e576 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/lang/UUID.java b/hutool-core/src/main/java/cn/hutool/core/lang/UUID.java new file mode 100644 index 000000000..5be8ec559 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/UUID.java @@ -0,0 +1,449 @@ +package cn.hutool.core.lang; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Random; + +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 提供通用唯一识别码(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 final class UUID implements java.io.Serializable, Comparable { + private static final long serialVersionUID = -1185015143654744140L; + + /** + * {@link SecureRandom} 的单例 + * + * @author looly + * + */ + private static class Holder { + static final SecureRandom numberGenerator = 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.numberGenerator : RandomUtil.getRandom(); + + 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]).longValue(); + mostSigBits <<= 16; + mostSigBits |= Long.decode(components[1]).longValue(); + mostSigBits <<= 16; + mostSigBits |= Long.decode(components[2]).longValue(); + + long leastSigBits = Long.decode(components[3]).longValue(); + leastSigBits <<= 48; + leastSigBits |= Long.decode(components[4]).longValue(); + + 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。 + * + * @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 (false == isSimple) { + builder.append('-'); + } + // time_mid + builder.append(digits(mostSigBits >> 16, 4)); + if (false == isSimple) { + builder.append('-'); + } + // time_high_and_version + builder.append(digits(mostSigBits, 4)); + if (false == isSimple) { + builder.append('-'); + } + // variant_and_sequence + builder.append(digits(leastSigBits >> 48, 4)); + if (false == isSimple) { + builder.append('-'); + } + // node + builder.append(digits(leastSigBits, 12)); + + return builder.toString(); + } + + /** + * 返回此 UUID 的哈希码。 + * + * @return UUID 的哈希码值。 + */ + 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} + */ + 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。 + * + */ + public int compareTo(UUID val) { + // The ordering is intentionally set up so that the UUIDs + // can simply be numerically compared as two numbers + return (this.mostSigBits < val.mostSigBits ? -1 : // + (this.mostSigBits > val.mostSigBits ? 1 : // + (this.leastSigBits < val.leastSigBits ? -1 : // + (this.leastSigBits > val.leastSigBits ? 1 : // + 0)))); + } + + // ------------------------------------------------------------------------------------------------------------------- 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/hutool-core/src/main/java/cn/hutool/core/lang/Validator.java b/hutool-core/src/main/java/cn/hutool/core/lang/Validator.java new file mode 100644 index 000000000..c9cf66efb --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/Validator.java @@ -0,0 +1,1069 @@ +package cn.hutool.core.lang; + +import java.net.MalformedURLException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.exceptions.ValidateException; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 字段验证器 + * + * @author Looly + * + */ +public class Validator { + + private 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; + + /** + * 给定值是否为true + * + * @param value 值 + * @return 是否为ture + * @since 4.4.5 + */ + public static boolean isTrue(boolean value) { + return value; + } + + /** + * 给定值是否不为false + * + * @param value 值 + * @return 是否不为false + * @since 4.4.5 + */ + public static boolean isFalse(boolean value) { + return false == value; + } + + /** + * 检查指定值是否为ture + * + * @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 value; + } + + /** + * 检查指定值是否为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 value; + } + + /** + * 给定值是否为null + * + * @param value 值 + * @return 是否为null + */ + public static boolean isNull(Object value) { + return null == value; + } + + /** + * 给定值是否不为null + * + * @param value 值 + * @return 是否不为null + */ + public static boolean isNotNull(Object value) { + return null != value; + } + + /** + * 检查指定值是否为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 value; + } + + /** + * 检查指定值是否非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 是否为空 + * @return 是否为空 + */ + public static boolean isEmpty(Object value) { + return (null == value || (value instanceof String && StrUtil.isEmpty((String) value))); + } + + /** + * 验证是否为非空
+ * 对于String类型判定是否为empty(null 或 "")
+ * + * @param value 值 + * @return 是否为空 + * @return 是否为空 + */ + public static boolean isNotEmpty(Object value) { + return false == isEmpty(value); + } + + /** + * 验证是否为空,非空时抛出异常
+ * 对于String类型判定是否为empty(null 或 "")
+ * + * @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 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 (false == 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); + } + + /** + * 通过正则表达式验证 + * + * @param regex 正则 + * @param value 值 + * @return 是否匹配正则 + */ + public static boolean isMactchRegex(String regex, CharSequence value) { + return ReUtil.isMatch(regex, value); + } + + /** + * 通过正则表达式验证
+ * 不符合正则抛出{@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 (false == isMactchRegex(regex, value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 通过正则表达式验证 + * + * @param pattern 正则模式 + * @param value 值 + * @return 是否匹配正则 + */ + public static boolean isMactchRegex(Pattern pattern, CharSequence value) { + return ReUtil.isMatch(pattern, value); + } + + /** + * 验证是否为英文字母 、数字和下划线 + * + * @param value 值 + * @return 是否为英文字母 、数字和下划线 + */ + public static boolean isGeneral(CharSequence value) { + return isMactchRegex(GENERAL, value); + } + + /** + * 验证是否为英文字母 、数字和下划线 + * + * @param 字符串类型 + * @param value 值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + */ + public static T validateGeneral(T value, String errorMsg) throws ValidateException { + if (false == 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) { + String reg = "^\\w{" + min + "," + max + "}$"; + if (min < 0) { + min = 0; + } + if (max <= 0) { + reg = "^\\w{" + min + ",}$"; + } + return isMactchRegex(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 (false == 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, new cn.hutool.core.lang.Matcher() { + @Override + public boolean match(Character t) { + return Character.isLetter(t); + } + }); + } + + /** + * 验证是否全部为字母组成,包括大写和小写字母和汉字 + * + * @param 字符串类型 + * @param value 表单值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + * @since 3.3.0 + */ + public static T validateLetter(T value, String errorMsg) throws ValidateException { + if (false == 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, new cn.hutool.core.lang.Matcher() { + @Override + public boolean match(Character t) { + return Character.isUpperCase(t); + } + }); + } + + /** + * 验证字符串是否全部为大写字母 + * + * @param 字符串类型 + * @param value 表单值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + * @since 3.3.0 + */ + public static T validateUpperCase(T value, String errorMsg) throws ValidateException { + if (false == 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, new cn.hutool.core.lang.Matcher() { + @Override + public boolean match(Character t) { + return Character.isLowerCase(t); + } + }); + } + + /** + * 验证字符串是否全部为小写字母 + * + * @param 字符串类型 + * @param value 表单值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + * @since 3.3.0 + */ + public static T validateLowerCase(T value, String errorMsg) throws ValidateException { + if (false == isLowerCase(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证该字符串是否是数字 + * + * @param value 字符串内容 + * @return 是否是数字 + */ + public static boolean isNumber(String value) { + return NumberUtil.isNumber(value); + } + + /** + * 验证是否为数字 + * + * @param value 表单值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + */ + public static String validateNumber(String value, String errorMsg) throws ValidateException { + if (false == isNumber(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证该字符串是否是字母(包括大写和小写字母) + * + * @param value 字符串内容 + * @return 是否是字母(包括大写和小写字母) + * @since 4.1.8 + */ + public static boolean isWord(CharSequence value) { + return isMactchRegex(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 (false == isWord(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证是否为货币 + * + * @param value 值 + * @return 是否为货币 + */ + public static boolean isMoney(CharSequence value) { + return isMactchRegex(MONEY, value); + } + + /** + * 验证是否为货币 + * + * @param 字符串类型 + * @param value 值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + */ + public static T validateMoney(T value, String errorMsg) throws ValidateException { + if (false == isMoney(value)) { + throw new ValidateException(errorMsg); + } + return value; + + } + + /** + * 验证是否为邮政编码(中国) + * + * @param value 值 + * @return 是否为邮政编码(中国) + */ + public static boolean isZipCode(CharSequence value) { + return isMactchRegex(ZIP_CODE, value); + } + + /** + * 验证是否为邮政编码(中国) + * + * @param 字符串类型 + * @param value 表单值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + */ + public static T validateZipCode(T value, String errorMsg) throws ValidateException { + if (false == isZipCode(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证是否为可用邮箱地址 + * + * @param value 值 + * @return 否为可用邮箱地址 + */ + public static boolean isEmail(CharSequence value) { + return isMactchRegex(EMAIL, value); + } + + /** + * 验证是否为可用邮箱地址 + * + * @param 字符串类型 + * @param value 值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + */ + public static T validateEmail(T value, String errorMsg) throws ValidateException { + if (false == isEmail(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证是否为手机号码(中国) + * + * @param value 值 + * @return 是否为手机号码(中国) + */ + public static boolean isMobile(CharSequence value) { + return isMactchRegex(MOBILE, value); + } + + /** + * 验证是否为手机号码(中国) + * + * @param 字符串类型 + * @param value 值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + */ + public static T validateMobile(T value, String errorMsg) throws ValidateException { + if (false == isMobile(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证是否为身份证号码(18位中国)
+ * 出生日期只支持到到2999年 + * + * @param value 值 + * @return 是否为身份证号码(18位中国) + */ + public static boolean isCitizenId(CharSequence value) { + return isMactchRegex(CITIZEN_ID, value); + } + + /** + * 验证是否为身份证号码(18位中国)
+ * 出生日期只支持到到2999年 + * + * @param 字符串类型 + * @param value 值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + */ + public static T validateCitizenIdNumber(T value, String errorMsg) throws ValidateException { + if (false == 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 ((month == 4 || month == 6 || month == 9 || month == 11) && day == 31) { + return false; + } + if (month == 2) { + if (day > 29 || (day == 29 && false == DateUtil.isLeapYear(year))) { + return false; + } + } + return true; + } + + /** + * 验证是否为生日
+ * 只支持以下几种格式: + *

    + *
  • yyyyMMdd
  • + *
  • yyyy-MM-dd
  • + *
  • yyyy/MM/dd
  • + *
  • yyyy.MM.dd
  • + *
  • yyyy年MM月dd日
  • + *
+ * + * @param value 值 + * @return 是否为生日 + */ + public static boolean isBirthday(CharSequence value) { + if (isMactchRegex(BIRTHDAY, value)) { + 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 (false == isBirthday(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证是否为IPV4地址 + * + * @param value 值 + * @return 是否为IPV4地址 + */ + public static boolean isIpv4(CharSequence value) { + return isMactchRegex(IPV4, value); + } + + /** + * 验证是否为IPV4地址 + * + * @param 字符串类型 + * @param value 值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + */ + public static T validateIpv4(T value, String errorMsg) throws ValidateException { + if (false == isIpv4(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证是否为IPV6地址 + * + * @param value 值 + * @return 是否为IPV6地址 + */ + public static boolean isIpv6(CharSequence value) { + return isMactchRegex(IPV6, value); + } + + /** + * 验证是否为IPV6地址 + * + * @param 字符串类型 + * @param value 值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + */ + public static T validateIpv6(T value, String errorMsg) throws ValidateException { + if (false == isIpv6(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证是否为MAC地址 + * + * @param value 值 + * @return 是否为MAC地址 + * @since 4.1.3 + */ + public static boolean isMac(CharSequence value) { + return isMactchRegex(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 (false == isMac(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证是否为中国车牌号 + * + * @param value 值 + * @return 是否为中国车牌号 + * @since 3.0.6 + */ + public static boolean isPlateNumber(CharSequence value) { + return isMactchRegex(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 (false == isPlateNumber(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证是否为URL + * + * @param value 值 + * @return 是否为URL + */ + public static boolean isUrl(CharSequence value) { + 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 (false == isUrl(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证是否为汉字 + * + * @param value 值 + * @return 是否为汉字 + */ + public static boolean isChinese(CharSequence value) { + return isMactchRegex("^" + ReUtil.RE_CHINESE + "+$", value); + } + + /** + * 验证是否为汉字 + * + * @param 字符串类型 + * @param value 表单值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + */ + public static T validateChinese(T value, String errorMsg) throws ValidateException { + if (false == isChinese(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证是否为中文字、英文字母、数字和下划线 + * + * @param value 值 + * @return 是否为中文字、英文字母、数字和下划线 + */ + public static boolean isGeneralWithChinese(CharSequence value) { + return isMactchRegex(GENERAL_WITH_CHINESE, value); + } + + /** + * 验证是否为中文字、英文字母、数字和下划线 + * + * @param 字符串类型 + * @param value 值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + */ + public static T validateGeneralWithChinese(T value, String errorMsg) throws ValidateException { + if (false == isGeneralWithChinese(value)) { + throw new ValidateException(errorMsg); + } + return value; + } + + /** + * 验证是否为UUID
+ * 包括带横线标准格式和不带横线的简单模式 + * + * @param value 值 + * @return 是否为UUID + */ + public static boolean isUUID(CharSequence value) { + return isMactchRegex(UUID, value) || isMactchRegex(UUID_SIMPLE, value); + } + + /** + * 验证是否为UUID
+ * 包括带横线标准格式和不带横线的简单模式 + * + * @param 字符串类型 + * @param value 值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + */ + public static T validateUUID(T value, String errorMsg) throws ValidateException { + if (false == 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 isMactchRegex(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 (false == 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 (false == isBetween(value, min, max)) { + throw new ValidateException(errorMsg); + } + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/WeightRandom.java b/hutool-core/src/main/java/cn/hutool/core/lang/WeightRandom.java new file mode 100644 index 000000000..efc0e4e2b --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/WeightRandom.java @@ -0,0 +1,236 @@ +package cn.hutool.core.lang; + +import java.io.Serializable; +import java.util.Random; +import java.util.SortedMap; +import java.util.TreeMap; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.RandomUtil; + +/** + * 权重随机算法实现
+ *

+ * 平时,经常会遇到权重随机算法,从不同权重的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 TreeMap weightMap; + private Random random; + + /** + * 创建权重随机获取器 + * + * @return {@link WeightRandom} + */ + public static WeightRandom create() { + return new WeightRandom<>(); + } + + // ---------------------------------------------------------------------------------- Constructor start + /** + * 构造 + */ + public WeightRandom() { + weightMap = new TreeMap<>(); + random = RandomUtil.getRandom(); + } + + /** + * 构造 + * + * @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 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 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; + } + if (Double.doubleToLongBits(weight) != Double.doubleToLongBits(other.weight)) { + return false; + } + return true; + } + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/caller/Caller.java b/hutool-core/src/main/java/cn/hutool/core/lang/caller/Caller.java new file mode 100644 index 000000000..4e2b4774d --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/lang/caller/CallerUtil.java b/hutool-core/src/main/java/cn/hutool/core/lang/caller/CallerUtil.java new file mode 100644 index 000000000..da051ff57 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/caller/CallerUtil.java @@ -0,0 +1,81 @@ +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 {@link CallerUtil}
+	 * 1 调用{@link CallerUtil}中方法的类
+	 * 2 调用者的调用者
+	 * ...
+	 * 
+ * + * @param depth 层级。0表示{@link CallerUtil}本身,1表示调用{@link 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); + } + + /** + * 尝试创建{@link Caller}实现 + * + * @return {@link Caller}实现 + */ + private static Caller tryCreateCaller() { + Caller caller = null; + 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/hutool-core/src/main/java/cn/hutool/core/lang/caller/SecurityManagerCaller.java b/hutool-core/src/main/java/cn/hutool/core/lang/caller/SecurityManagerCaller.java new file mode 100644 index 000000000..3a8b1ca57 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/lang/caller/StackTraceCaller.java b/hutool-core/src/main/java/cn/hutool/core/lang/caller/StackTraceCaller.java new file mode 100644 index 000000000..422612fc2 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/caller/StackTraceCaller.java @@ -0,0 +1,70 @@ +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 (null == stackTrace || (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 (null == stackTrace || (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 (null == stackTrace || (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(); + if(null != stackTrace) { + for (final StackTraceElement element : stackTrace) { + if (element.getClassName().equals(clazz.getName())) { + return true; + } + } + } + return false; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/caller/package-info.java b/hutool-core/src/main/java/cn/hutool/core/lang/caller/package-info.java new file mode 100644 index 000000000..4239befa2 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/lang/copier/Copier.java b/hutool-core/src/main/java/cn/hutool/core/lang/copier/Copier.java new file mode 100644 index 000000000..6ba734f5e --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/copier/Copier.java @@ -0,0 +1,15 @@ +package cn.hutool.core.lang.copier; + +/** + * 拷贝接口 + * @author Looly + * + * @param 拷贝目标类型 + */ +public interface Copier { + /** + * 执行拷贝 + * @return 拷贝的目标 + */ + T copy(); +} diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/copier/SrcToDestCopier.java b/hutool-core/src/main/java/cn/hutool/core/lang/copier/SrcToDestCopier.java new file mode 100644 index 000000000..26e667f55 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/lang/copier/package-info.java b/hutool-core/src/main/java/cn/hutool/core/lang/copier/package-info.java new file mode 100644 index 000000000..1e37fc7e5 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/lang/func/Func.java b/hutool-core/src/main/java/cn/hutool/core/lang/func/Func.java new file mode 100644 index 000000000..595601961 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/func/Func.java @@ -0,0 +1,25 @@ +package cn.hutool.core.lang.func; + +/** + * 函数对象
+ * 接口灵感来自于ActFramework
+ * 一个函数接口代表一个一个函数,用于包装一个函数为对象
+ * 在JDK8之前,Java的函数并不能作为参数传递,也不能作为返回值存在,此接口用于将一个函数包装成为一个对象,从而传递对象 + * + * @author Looly + * + * @param

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

参数类型 + * @param 返回值类型 + * @since 4.2.2 + */ +public interface Func1 { + + /** + * 执行函数 + * + * @param parameter 参数 + * @return 函数执行结果 + * @throws Exception 自定义异常 + */ + R call(P parameter) throws Exception; +} diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/func/VoidFunc.java b/hutool-core/src/main/java/cn/hutool/core/lang/func/VoidFunc.java new file mode 100644 index 000000000..fb75fe15c --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/func/VoidFunc.java @@ -0,0 +1,24 @@ +package cn.hutool.core.lang.func; + +/** + * 函数对象
+ * 接口灵感来自于ActFramework
+ * 一个函数接口代表一个一个函数,用于包装一个函数为对象
+ * 在JDK8之前,Java的函数并不能作为参数传递,也不能作为返回值存在,此接口用于将一个函数包装成为一个对象,从而传递对象 + * + * @author Looly + * + * @param

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

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

{ + + /** + * 执行函数 + * + * @param parameter 参数 + * @throws Exception 自定义异常 + */ + void call(P parameter) throws Exception; +} diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/func/package-info.java b/hutool-core/src/main/java/cn/hutool/core/lang/func/package-info.java new file mode 100644 index 000000000..56c59d9e2 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/lang/loader/AtomicLoader.java b/hutool-core/src/main/java/cn/hutool/core/lang/loader/AtomicLoader.java new file mode 100644 index 000000000..c1074e89b --- /dev/null +++ b/hutool-core/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 (false == reference.compareAndSet(null, result)) { + // 其它线程已经创建好此对象 + result = reference.get(); + } + } + + return result; + } + + /** + * 初始化被加载的对象
+ * 如果对象从未被加载过,调用此方法初始化加载对象,此方法只被调用一次 + * + * @return 被加载的对象 + */ + protected abstract T init(); +} diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/loader/LazyLoader.java b/hutool-core/src/main/java/cn/hutool/core/lang/loader/LazyLoader.java new file mode 100644 index 000000000..be0823fdd --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/lang/loader/Loader.java b/hutool-core/src/main/java/cn/hutool/core/lang/loader/Loader.java new file mode 100644 index 000000000..02c06611e --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/loader/Loader.java @@ -0,0 +1,20 @@ +package cn.hutool.core.lang.loader; + +/** + * 对象加载抽象接口
+ * 通过实现此接口自定义实现对象的加载方式,例如懒加载机制、多线程加载等 + * + * @author looly + * + * @param 对象类型 + */ +public interface Loader { + + /** + * 获取一个准备好的对象
+ * 通过准备逻辑准备好被加载的对象,然后返回。在准备完毕之前此方法应该被阻塞 + * + * @return 加载完毕的对象 + */ + T get(); +} diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/loader/package-info.java b/hutool-core/src/main/java/cn/hutool/core/lang/loader/package-info.java new file mode 100644 index 000000000..4a44ecca3 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/lang/mutable/Mutable.java b/hutool-core/src/main/java/cn/hutool/core/lang/mutable/Mutable.java new file mode 100644 index 000000000..b81f8098e --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableBool.java b/hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableBool.java new file mode 100644 index 000000000..1d159131e --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableBool.java @@ -0,0 +1,103 @@ +package cn.hutool.core.lang.mutable; + +import java.io.Serializable; + +/** + * 可变 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() { + super(); + } + + /** + * 构造 + * @param value 值 + */ + public MutableBool(final boolean value) { + super(); + this.value = value; + } + + /** + * 构造 + * @param value String值 + * @throws NumberFormatException 转为Boolean错误 + */ + public MutableBool(final String value) throws NumberFormatException { + super(); + this.value = Boolean.parseBoolean(value); + } + + @Override + public Boolean get() { + return Boolean.valueOf(this.value); + } + + /** + * 设置值 + * @param value 值 + */ + public void set(final boolean value) { + this.value = value; + } + + @Override + public void set(final Boolean value) { + this.value = value.booleanValue(); + } + + // ----------------------------------------------------------------------- + /** + * 相等需同时满足如下条件: + *
    + *
  1. 非空
  2. + *
  3. 类型为 {@link MutableBool}
  4. + *
  5. 值相等
  6. + *
+ * + * @param obj 比对的对象 + * @return 相同返回true,否则 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 其它 {@link 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/hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableByte.java b/hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableByte.java new file mode 100644 index 000000000..3a2bb6366 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableByte.java @@ -0,0 +1,201 @@ +package cn.hutool.core.lang.mutable; + +import cn.hutool.core.util.NumberUtil; + +/** + * 可变 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() { + super(); + } + + /** + * 构造 + * @param value 值 + */ + public MutableByte(final byte value) { + super(); + 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 { + super(); + this.value = Byte.parseByte(value); + } + + @Override + public Byte get() { + return Byte.valueOf(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. 类型为 {@link MutableByte}
  4. + *
  5. 值相等
  6. + *
+ * + * @param obj 比对的对象 + * @return 相同返回true,否则 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 其它 {@link 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/hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableDouble.java b/hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableDouble.java new file mode 100644 index 000000000..5a82558eb --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableDouble.java @@ -0,0 +1,195 @@ +package cn.hutool.core.lang.mutable; + +import cn.hutool.core.util.NumberUtil; + +/** + * 可变 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() { + super(); + } + + /** + * 构造 + * @param value 值 + */ + public MutableDouble(final double value) { + super(); + 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 { + super(); + this.value = Double.parseDouble(value); + } + + @Override + public Double get() { + return Double.valueOf(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. 类型为 {@link MutableDouble}
  4. + *
  5. 值相等
  6. + *
+ * + * @param obj 比对的对象 + * @return 相同返回true,否则 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 其它 {@link 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/hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableFloat.java b/hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableFloat.java new file mode 100644 index 000000000..a4115019f --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableFloat.java @@ -0,0 +1,196 @@ +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() { + super(); + } + + /** + * 构造 + * @param value 值 + */ + public MutableFloat(final float value) { + super(); + 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 { + super(); + this.value = Float.parseFloat(value); + } + + @Override + public Float get() { + return Float.valueOf(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/hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableInt.java b/hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableInt.java new file mode 100644 index 000000000..8d62770e3 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableInt.java @@ -0,0 +1,196 @@ +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() { + super(); + } + + /** + * 构造 + * @param value 值 + */ + public MutableInt(final int value) { + super(); + 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 { + super(); + this.value = Integer.parseInt(value); + } + + @Override + public Integer get() { + return Integer.valueOf(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. 类型为 {@link MutableInt}
  4. + *
  5. 值相等
  6. + *
+ * + * @param obj 比对的对象 + * @return 相同返回true,否则 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 其它 {@link 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/hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableLong.java b/hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableLong.java new file mode 100644 index 000000000..cc8f93407 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableLong.java @@ -0,0 +1,196 @@ +package cn.hutool.core.lang.mutable; + +import cn.hutool.core.util.NumberUtil; + +/** + * 可变 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() { + super(); + } + + /** + * 构造 + * @param value 值 + */ + public MutableLong(final long value) { + super(); + 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 { + super(); + this.value = Long.parseLong(value); + } + + @Override + public Long get() { + return Long.valueOf(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. 类型为 {@link MutableLong}
  4. + *
  5. 值相等
  6. + *
+ * + * @param obj 比对的对象 + * @return 相同返回true,否则 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 其它 {@link 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/hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableObj.java b/hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableObj.java new file mode 100644 index 000000000..08baa1c3a --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableObj.java @@ -0,0 +1,71 @@ +package cn.hutool.core.lang.mutable; + +import java.io.Serializable; + +/** + * 可变Object + * + * @param 可变的类型 + * @since 3.0.1 + */ +public class MutableObj implements Mutable, Serializable { + private static final long serialVersionUID = 1L; + + private T value; + + /** + * 构造,空值 + */ + public MutableObj() { + super(); + } + + /** + * 构造 + * + * @param value 值 + */ + public MutableObj(final T value) { + super(); + 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 this.value.equals(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/hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableShort.java b/hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableShort.java new file mode 100644 index 000000000..4a0b21f40 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/lang/mutable/MutableShort.java @@ -0,0 +1,201 @@ +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() { + super(); + } + + /** + * 构造 + * @param value 值 + */ + public MutableShort(final short value) { + super(); + 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 { + super(); + this.value = Short.parseShort(value); + } + + @Override + public Short get() { + return Short.valueOf(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/hutool-core/src/main/java/cn/hutool/core/lang/mutable/package-info.java b/hutool-core/src/main/java/cn/hutool/core/lang/mutable/package-info.java new file mode 100644 index 000000000..cf41c7dab --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/lang/package-info.java b/hutool-core/src/main/java/cn/hutool/core/lang/package-info.java new file mode 100644 index 000000000..498689211 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/map/CamelCaseLinkedMap.java b/hutool-core/src/main/java/cn/hutool/core/map/CamelCaseLinkedMap.java new file mode 100644 index 000000000..47f0e00b3 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/map/CamelCaseLinkedMap.java @@ -0,0 +1,66 @@ +package cn.hutool.core.map; + +import java.util.HashMap; +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 + */ + 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 HashMap(initialCapacity, loadFactor)); + } + // ------------------------------------------------------------------------- Constructor end +} diff --git a/hutool-core/src/main/java/cn/hutool/core/map/CamelCaseMap.java b/hutool-core/src/main/java/cn/hutool/core/map/CamelCaseMap.java new file mode 100644 index 000000000..8dc8d4faf --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/map/CamelCaseMap.java @@ -0,0 +1,82 @@ +package cn.hutool.core.map; + +import java.util.HashMap; +import java.util.Map; + +import cn.hutool.core.util.StrUtil; + +/** + * 驼峰Key风格的Map
+ * 对KEY转换为驼峰,get("int_value")和get("intValue")获得的值相同,put进入的值也会被覆盖 + * + * @author Looly + * + * @param 键类型 + * @param 值类型 + * @since 4.0.7 + */ +public class CamelCaseMap extends CustomKeyMap { + 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 + */ + public CamelCaseMap(float loadFactor, Map m) { + this(m.size(), loadFactor); + this.putAll(m); + } + + /** + * 构造 + * + * @param initialCapacity 初始大小 + * @param loadFactor 加载因子 + */ + public CamelCaseMap(int initialCapacity, float loadFactor) { + super(new HashMap(initialCapacity, loadFactor)); + } + // ------------------------------------------------------------------------- Constructor end + + /** + * 将Key转为驼峰风格,如果key为字符串的话 + * + * @param key KEY + * @return 驼峰Key + */ + @Override + protected Object customKey(Object key) { + if (null != key && key instanceof CharSequence) { + key = StrUtil.toCamelCase(key.toString()); + } + return key; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/map/CaseInsensitiveLinkedMap.java b/hutool-core/src/main/java/cn/hutool/core/map/CaseInsensitiveLinkedMap.java new file mode 100644 index 000000000..8bc486ee2 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/map/CaseInsensitiveMap.java b/hutool-core/src/main/java/cn/hutool/core/map/CaseInsensitiveMap.java new file mode 100644 index 000000000..7ca6fd3b1 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/map/CaseInsensitiveMap.java @@ -0,0 +1,81 @@ +package cn.hutool.core.map; + +import java.util.HashMap; +import java.util.Map; + +/** + * 忽略大小写的Map
+ * 对KEY忽略大小写,get("Value")和get("value")获得的值相同,put进入的值也会被覆盖 + * + * @author Looly + * + * @param 键类型 + * @param 值类型 + * @since 3.0.2 + */ +public class CaseInsensitiveMap extends CustomKeyMap { + 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); + } + + /** + * 构造 + * + * @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) { + super(new HashMap(initialCapacity, loadFactor)); + } + //------------------------------------------------------------------------- Constructor end + + /** + * 将Key转为小写 + * + * @param key KEY + * @return 小写KEY + */ + @Override + protected Object customKey(Object key) { + if (null != key && key instanceof CharSequence) { + key = key.toString().toLowerCase(); + } + return key; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/map/CustomKeyMap.java b/hutool-core/src/main/java/cn/hutool/core/map/CustomKeyMap.java new file mode 100644 index 000000000..efe6637fc --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/map/CustomKeyMap.java @@ -0,0 +1,58 @@ +package cn.hutool.core.map; + +import java.util.Map; + +/** + * 自定义键的Map,默认HashMap实现 + * + * @author Looly + * + * @param 键类型 + * @param 值类型 + * @since 4.0.7 + */ +public abstract class CustomKeyMap extends MapWrapper { + private static final long serialVersionUID = 4043263744224569870L; + + /** + * 构造
+ * 通过传入一个Map从而确定Map的类型,子类需创建一个空的Map,而非传入一个已有Map,否则值可能会被修改 + * + * @param m Map 被包装的Map + * @since 3.1.2 + */ + public CustomKeyMap(Map m) { + super(m); + } + + @Override + public V get(Object key) { + return super.get(customKey(key)); + } + + @SuppressWarnings("unchecked") + @Override + public V put(K key, V value) { + return super.put((K) customKey(key), value); + } + + @Override + public void putAll(Map m) { + for (Map.Entry entry : m.entrySet()) { + this.put(entry.getKey(), entry.getValue()); + } + } + + @Override + public boolean containsKey(Object key) { + return super.containsKey(customKey(key)); + } + + /** + * 自定义键 + * + * @param key KEY + * @return 自定义KEY + */ + protected abstract Object customKey(Object key); +} diff --git a/hutool-core/src/main/java/cn/hutool/core/map/FixedLinkedHashMap.java b/hutool-core/src/main/java/cn/hutool/core/map/FixedLinkedHashMap.java new file mode 100644 index 000000000..3376838bd --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/map/FixedLinkedHashMap.java @@ -0,0 +1,53 @@ +package cn.hutool.core.map; + +import java.util.LinkedHashMap; + +/** + * 固定大小的{@link LinkedHashMap} 实现 + * + * @author looly + * + * @param 键类型 + * @param 值类型 + */ +public class FixedLinkedHashMap extends LinkedHashMap { + private static final long serialVersionUID = -629171177321416095L; + + /** 容量,超过此容量自动删除末尾元素 */ + private int capacity; + + /** + * 构造 + * + * @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; + } + + @Override + protected boolean removeEldestEntry(java.util.Map.Entry eldest) { + //当链表元素大于容量时,移除最老(最久未被使用)的元素 + return size() > this.capacity; + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/map/MapBuilder.java b/hutool-core/src/main/java/cn/hutool/core/map/MapBuilder.java new file mode 100644 index 000000000..a278e5521 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/map/MapBuilder.java @@ -0,0 +1,116 @@ +package cn.hutool.core.map; + +import java.io.Serializable; +import java.util.Map; + +/** + * Map创建类 + * + * @param Key类型 + * @param Value类型 + * @since 3.1.1 + */ +public class MapBuilder implements Serializable{ + private static final long serialVersionUID = 1L; + + private Map map; + + /** + * 创建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 map 合并map + * @return 当前类 + */ + public MapBuilder putAll(Map map) { + this.map.putAll(map); + return this; + } + + /** + * 创建后的map + * + * @return 创建后的map + */ + public Map map() { + return map; + } + + /** + * 创建后的map + * + * @return 创建后的map + * @since 3.3.0 + */ + 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); + } + +} \ No newline at end of file diff --git a/hutool-core/src/main/java/cn/hutool/core/map/MapProxy.java b/hutool-core/src/main/java/cn/hutool/core/map/MapProxy.java new file mode 100644 index 000000000..916266ed0 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/map/MapProxy.java @@ -0,0 +1,178 @@ +package cn.hutool.core.map; + +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; + +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; + +/** + * Map代理,提供各种getXXX方法,并提供默认值支持 + * + * @author looly + * @since 3.2.0 + */ +public class MapProxy extends OptNullBasicTypeFromObjectGetter implements Map, 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") + @Override + public void putAll(Map m) { + map.putAll(m); + } + + @Override + public void clear() { + map.clear(); + } + + @SuppressWarnings("unchecked") + @Override + public Set keySet() { + return map.keySet(); + } + + @SuppressWarnings("unchecked") + @Override + public Collection values() { + return map.values(); + } + + @SuppressWarnings("unchecked") + @Override + public Set> entrySet() { + return map.entrySet(); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + final Class[] parameterTypes = method.getParameterTypes(); + if (ArrayUtil.isEmpty(parameterTypes)) { + final Class returnType = method.getReturnType(); + if (null != returnType && 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 (false == 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]); + } + } else if ("equals".equals(methodName)) { + return this.equals(args[0]); + } + } + + throw new UnsupportedOperationException(method.toGenericString()); + } + + /** + * 将Map代理为指定接口的动态代理对象 + * + * @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/hutool-core/src/main/java/cn/hutool/core/map/MapUtil.java b/hutool-core/src/main/java/cn/hutool/core/map/MapUtil.java new file mode 100644 index 000000000..9a66d9dad --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/map/MapUtil.java @@ -0,0 +1,906 @@ +package cn.hutool.core.map; + +import java.util.AbstractMap; +import java.util.ArrayList; +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.Set; +import java.util.TreeMap; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Editor; +import cn.hutool.core.lang.Filter; +import cn.hutool.core.lang.TypeReference; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 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 && false == map.isEmpty(); + } + + // ----------------------------------------------------------------------------------------------- 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 isOrder Map的Key是否有序,有序返回 {@link LinkedHashMap},否则返回 {@link HashMap} + * @return HashMap对象 + * @since 3.0.4 + */ + public static HashMap newHashMap(int size, boolean isOrder) { + int initialCapacity = (int) (size / DEFAULT_LOAD_FACTOR) + 1; + return isOrder ? 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 isOrder Map的Key是否有序,有序返回 {@link LinkedHashMap},否则返回 {@link HashMap} + * @return HashMap对象 + */ + public static HashMap newHashMap(boolean isOrder) { + return newHashMap(DEFAULT_INITIAL_CAPACITY, isOrder); + } + + /** + * 新建TreeMap,Key有序的Map + * + * @param comparator Key比较器 + * @return TreeMap + * @since 3.2.3 + */ + public static TreeMap newTreeMap(Comparator comparator) { + return new TreeMap<>(comparator); + } + + /** + * 新建TreeMap,Key有序的Map + * + * @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 (false == isEmpty(map)) { + treeMap.putAll(map); + } + return treeMap; + } + + /** + * 创建键不重复Map + * + * @return {@link IdentityHashMap} + * @since 4.5.7 + */ + public static Map newIdentityMap(int size){ + return new IdentityHashMap<>(size); + } + + /** + * 创建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 (mapType.isAssignableFrom(AbstractMap.class)) { + return new HashMap<>(); + } else { + return (Map) ReflectUtil.newInstance(mapType); + } + } + + // ----------------------------------------------------------------------------------------------- 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; + } + + /** + * 将数组转换为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++) { + Object object = array[i]; + if (object instanceof Map.Entry) { + Map.Entry entry = (Map.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) { + 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) { + 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 (CollectionUtil.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 = CollectionUtil.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 = true;// 是否结束。标准是元素列表已耗尽 + int index = 0;// 值索引 + Map map; + do { + isEnd = true; + map = new HashMap<>(); + List vList; + int vListSize; + for (Entry> entry : listMap.entrySet()) { + vList = CollectionUtil.newArrayList(entry.getValue()); + vListSize = vList.size(); + if (index < vListSize) { + map.put(entry.getKey(), vList.get(index)); + if (index != vListSize - 1) { + // 当值列表中还有更多值(非最后一个),继续循环 + isEnd = false; + } + } + } + if (false == map.isEmpty()) { + resultList.add(map); + } + index++; + } while (false == isEnd); + + return resultList; + } + + /** + * 将已知Map转换为key为驼峰风格的Map
+ * 如果KEY为非String类型,保留原值 + * + * @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 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之间的连接符 + * @return 连接字符串 + * @since 3.1.1 + */ + public static String join(Map map, String separator, String keyValueSeparator) { + return join(map, separator, keyValueSeparator, false); + } + + /** + * 将map转成字符串,忽略null的键和值 + * + * @param 键类型 + * @param 值类型 + * @param map Map + * @param separator entry之间的连接符 + * @param keyValueSeparator kv之间的连接符 + * @return 连接后的字符串 + * @since 3.1.1 + */ + public static String joinIgnoreNull(Map map, String separator, String keyValueSeparator) { + return join(map, separator, keyValueSeparator, true); + } + + /** + * 将map转成字符串 + * + * @param 键类型 + * @param 值类型 + * @param map Map + * @param separator entry之间的连接符 + * @param keyValueSeparator kv之间的连接符 + * @param isIgnoreNull 是否忽略null的键和值 + * @return 连接后的字符串 + * @since 3.1.1 + */ + public static String join(Map map, String separator, String keyValueSeparator, boolean isIgnoreNull) { + final StringBuilder strBuilder = StrUtil.builder(); + boolean isFirst = true; + for (Entry entry : map.entrySet()) { + if (false == 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())); + } + } + return strBuilder.toString(); + } + + // ----------------------------------------------------------------------------------------------- filter + /** + * 过滤
+ * 过滤过程通过传入的Editor实现来返回需要的元素内容,这个Editor实现可以实现以下功能: + * + *
+	 * 1、过滤出需要的对象,如果返回null表示这个元素对象抛弃
+	 * 2、修改元素对象,返回集合中为修改后的对象
+	 * 
+ * + * @param Key类型 + * @param Value类型 + * @param map Map + * @param editor 编辑器接口 + * @return 过滤后的Map + */ + public static Map filter(Map map, Editor> editor) { + if(null == map || null == editor) { + return map; + } + + final Map map2 = ObjectUtil.clone(map); + if (isEmpty(map2)) { + return map2; + } + + map2.clear(); + 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 编辑器接口 + * @return 过滤后的Map + * @since 3.1.0 + */ + public static Map filter(Map map, Filter> filter) { + if(null == map || null == filter) { + return map; + } + + final Map map2 = ObjectUtil.clone(map); + if (isEmpty(map2)) { + return map2; + } + + map2.clear(); + for (Entry entry : map.entrySet()) { + if (filter.accept(entry)) { + map2.put(entry.getKey(), entry.getValue()); + } + } + return map2; + } + + /** + * 过滤Map保留指定键值对,如果键不存在跳过 + * + * @param Key类型 + * @param Value类型 + * @param map 原始Map + * @param keys 键列表 + * @return Map 结果,结果的Map类型与原Map保持一致 + * @since 4.0.10 + */ + @SuppressWarnings("unchecked") + public static Map filter(Map map, K... keys) { + final Map map2 = ObjectUtil.clone(map); + if (isEmpty(map2)) { + return map2; + } + + map2.clear(); + for (K key : keys) { + if(map.containsKey(key)) { + map2.put(key, map.get(key)); + } + } + return map2; + } + + /** + * Map的键和值互换 + * + * @param 键和值类型 + * @param map Map对象,键值类型必须一致 + * @return 互换后的Map + * @since 3.2.2 + */ + public static Map reverse(Map map) { + return filter(map, new Editor>() { + @Override + public Entry edit(final Entry t) { + return 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的key和value + * + * @param 键类型,目标的值类型 + * @param 值类型,目标的键类型 + * @param map 被转换的Map + * @return 逆转后的Map + * @deprecated 请使用{@link MapUtil#reverse(Map)} 代替 + */ + @Deprecated + public static Map inverse(Map map) { + Map inverseMap; + if (map instanceof LinkedHashMap) { + inverseMap = new LinkedHashMap<>(map.size()); + } else if (map instanceof TreeMap) { + inverseMap = new TreeMap<>(); + } else { + inverseMap = new HashMap<>(map.size()); + } + + for (Entry entry : map.entrySet()) { + inverseMap.put(entry.getValue(), entry.getKey()); + } + return inverseMap; + } + + /** + * 排序已有Map,Key有序的Map,使用默认Key排序方式(字母顺序) + * + * @param map Map + * @return TreeMap + * @since 4.0.1 + * @see #newTreeMap(Map, Comparator) + */ + public static TreeMap sort(Map map) { + return sort(map, null); + } + + /** + * 排序已有Map,Key有序的Map + * + * @param map Map + * @param comparator Key比较器 + * @return TreeMap + * @since 4.0.1 + * @see #newTreeMap(Map, Comparator) + */ + public static TreeMap sort(Map map, Comparator comparator) { + TreeMap result; + if (map instanceof TreeMap) { + // 已经是可排序Map,此时只有比较器一致才返回原map + result = (TreeMap) map; + if (null == comparator || comparator.equals(result.comparator())) { + return result; + } + } else { + result = newTreeMap(map, comparator); + } + + 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 map 被代理的Map + * @return {@link MapWrapper} + * @since 4.5.4 + */ + public static MapWrapper wrap(Map map) { + return new MapWrapper(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, new Filter>() { + + @Override + public boolean accept(Entry entry) { + return ArrayUtil.contains(keys, entry.getKey()); + } + }); + } + + /** + * 获取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的值,并转换为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的值,并转换为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的值,并转换为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的值,并转换为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的值,并转换为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的值,并转换为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的值,并转换为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的值,并转换为{@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的值,并转换为指定类型 + * + * @param 目标值类型 + * @param map Map + * @param key 键 + * @param type 值类型 + * @return 值 + * @since 4.0.6 + */ + public static T get(Map map, Object key, Class type) { + return null == map ? null : Convert.convert(type, map.get(key)); + } + + /** + * 获取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 null == map ? null : Convert.convert(type, map.get(key)); + } + + /** + * 重命名键
+ * 实现方式为一处然后重新put,当旧的key不存在直接返回
+ * 当新的key存在,抛出{@link IllegalArgumentException} 异常 + * + * @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; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/map/MapWrapper.java b/hutool-core/src/main/java/cn/hutool/core/map/MapWrapper.java new file mode 100644 index 000000000..436a0dbde --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/map/MapWrapper.java @@ -0,0 +1,111 @@ +package cn.hutool.core.map; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +/** + * Map包装类,通过包装一个已有Map实现特定功能。例如自定义Key的规则或Value规则 + * + * @author looly + * + * @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; + + /** + * 构造 + * + * @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 Set keySet() { + return raw.keySet(); + } + + @Override + public Collection values() { + return raw.values(); + } + + @Override + public Set> entrySet() { + return raw.entrySet(); + } + + @Override + public Iterator> iterator() { + return this.entrySet().iterator(); + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/map/TableMap.java b/hutool-core/src/main/java/cn/hutool/core/map/TableMap.java new file mode 100644 index 000000000..d2d0eb588 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/map/TableMap.java @@ -0,0 +1,154 @@ +package cn.hutool.core.map; + +import java.io.Serializable; +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 cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ArrayUtil; + +/** + * 无重复键的Map + * @author looly + * + * @param + * @param + */ +public class TableMap implements Map, Serializable { + private static final long serialVersionUID = -5852304468076152385L; + + private List keys; + private List values; + + /** + * 构造 + * + * @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 ArrayUtil.isEmpty(keys); + } + + @Override + public boolean containsKey(Object key) { + return keys.contains(key); + } + + @Override + public boolean containsValue(Object value) { + return values.contains(value); + } + + @Override + public V get(Object key) { + final int index = keys.indexOf(key); + if (index > -1 && index < values.size()) { + return values.get(index); + } + return null; + } + + @Override + public V put(K key, V value) { + keys.add(key); + values.add(value); + return null; + } + + @Override + public V remove(Object key) { + int index = keys.indexOf(key); + if (index > -1) { + keys.remove(index); + if (index < values.size()) { + values.remove(index); + } + } + return null; + } + + @Override + public void putAll(Map m) { + for (Map.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<>(keys); + } + + @Override + public Collection values() { + return new HashSet<>(values); + } + + @Override + public Set> entrySet() { + HashSet> hashSet = new HashSet<>(); + for(int i = 0; i < size(); i++) { + hashSet.add(new Entry(keys.get(i), values.get(i))); + } + return hashSet; + } + + private static class Entry implements Map.Entry{ + + private K key; + private V value; + + public Entry(K key, V value) { + this.key = key; + this.value = value; + } + + @Override + public K getKey() { + return key; + } + + @Override + public V getValue() { + return value; + } + + @Override + public V setValue(V value) { + throw new UnsupportedOperationException("setValue not supported."); + } + + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/map/multi/CollectionValueMap.java b/hutool-core/src/main/java/cn/hutool/core/map/multi/CollectionValueMap.java new file mode 100644 index 000000000..dab043fd2 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/map/multi/CollectionValueMap.java @@ -0,0 +1,109 @@ +package cn.hutool.core.map.multi; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapWrapper; + +/** + * 值作为集合的Map实现,通过调用putValue可以在相同key时加入多个值,多个值用集合表示 + * + * @author looly + * + * @param 键类型 + * @param 值类型 + * @since 4.3.3 + */ +public abstract class CollectionValueMap extends MapWrapper> { + + private static final long serialVersionUID = 9012989578038102983L; + + /** 默认集合初始大小 */ + protected static final int DEFAULT_COLLCTION_INITIAL_CAPACITY = 3; + + // ------------------------------------------------------------------------- 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(m.size(), loadFactor); + this.putAll(m); + } + + /** + * 构造 + * + * @param initialCapacity 初始大小 + * @param loadFactor 加载因子 + */ + public CollectionValueMap(int initialCapacity, float loadFactor) { + super(new HashMap>(initialCapacity, loadFactor)); + } + // ------------------------------------------------------------------------- Constructor end + + /** + * 放入Value
+ * 如果键对应值列表有值,加入,否则创建一个新列表后加入 + * + * @param key 键 + * @param value 值 + */ + public void putValue(K key, V value) { + Collection collection = this.get(key); + if (null == collection) { + collection = createCollction(); + 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); + } + + /** + * 创建集合
+ * 此方法用于创建在putValue后追加值所在的集合,子类实现此方法创建不同类型的集合 + * + * @return {@link Collection} + */ + protected abstract Collection createCollction(); +} diff --git a/hutool-core/src/main/java/cn/hutool/core/map/multi/ListValueMap.java b/hutool-core/src/main/java/cn/hutool/core/map/multi/ListValueMap.java new file mode 100644 index 000000000..72b705bbd --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/map/multi/ListValueMap.java @@ -0,0 +1,78 @@ +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 CollectionValueMap { + 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.putAll(m); + } + + /** + * 构造 + * + * @param initialCapacity 初始大小 + * @param loadFactor 加载因子 + */ + public ListValueMap(int initialCapacity, float loadFactor) { + super(new HashMap>(initialCapacity, loadFactor)); + } + // ------------------------------------------------------------------------- Constructor end + + @Override + public List get(Object key) { + return (List) super.get(key); + } + + @Override + protected Collection createCollction() { + return new ArrayList<>(DEFAULT_COLLCTION_INITIAL_CAPACITY); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/map/multi/SetValueMap.java b/hutool-core/src/main/java/cn/hutool/core/map/multi/SetValueMap.java new file mode 100644 index 000000000..52efdc4dc --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/map/multi/SetValueMap.java @@ -0,0 +1,78 @@ +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 CollectionValueMap { + 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.putAll(m); + } + + /** + * 构造 + * + * @param initialCapacity 初始大小 + * @param loadFactor 加载因子 + */ + public SetValueMap(int initialCapacity, float loadFactor) { + super(new HashMap>(initialCapacity, loadFactor)); + } + // ------------------------------------------------------------------------- Constructor end + + @Override + public Set get(Object key) { + return (Set) super.get(key); + } + + @Override + protected Collection createCollction() { + return new LinkedHashSet<>(DEFAULT_COLLCTION_INITIAL_CAPACITY); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/map/multi/package-info.java b/hutool-core/src/main/java/cn/hutool/core/map/multi/package-info.java new file mode 100644 index 000000000..aee4bd5aa --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/map/multi/package-info.java @@ -0,0 +1,7 @@ +/** + * 列表类型值的Map实现 + * + * @author looly + * + */ +package cn.hutool.core.map.multi; \ No newline at end of file diff --git a/hutool-core/src/main/java/cn/hutool/core/map/package-info.java b/hutool-core/src/main/java/cn/hutool/core/map/package-info.java new file mode 100644 index 000000000..d74c8b484 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/math/Arrangement.java b/hutool-core/src/main/java/cn/hutool/core/math/Arrangement.java new file mode 100644 index 000000000..b442578aa --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/math/Arrangement.java @@ -0,0 +1,133 @@ +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; + +/** + * 排列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 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(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 dataList 待选列表 + * @param resultList 前面(resultIndex-1)个的排列结果 + * @param resultIndex 选择索引,从0开始 + * @param result 最终结果 + */ + private void select(String[] resultList, int resultIndex, List result) { + int resultLen = resultList.length; + if (resultIndex >= resultLen) { // 全部选择完时,输出排列结果 + result.add(Arrays.copyOf(resultList, resultList.length)); + return; + } + + // 递归选择下一个 + for (int i = 0; i < datas.length; i++) { + // 判断待选项是否存在于排列结果中 + boolean exists = false; + for (int j = 0; j < resultIndex; j++) { + if (datas[i].equals(resultList[j])) { + exists = true; + break; + } + } + if (false == exists) { // 排列结果不存在该项,才可选择 + resultList[resultIndex] = datas[i]; + select(resultList, resultIndex + 1, result); + } + } + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/math/Combination.java b/hutool-core/src/main/java/cn/hutool/core/math/Combination.java new file mode 100644 index 000000000..7aa7d9136 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/math/Combination.java @@ -0,0 +1,110 @@ +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; + +/** + * 组合,即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 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) { + return 1; + } + if(n == m) { + return NumberUtil.factorial(n) / NumberUtil.factorial(m); + } + 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) { + long total = 0; + for(int i = 1; i <= n; i++) { + total += count(n, i); + } + return total; + } + + /** + * 组合选择(从列表中选择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 dataList 待选列表 + * @param dataIndex 待选开始索引 + * @param resultList 前面(resultIndex-1)个的组合结果 + * @param resultIndex 选择索引,从0开始 + */ + 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/hutool-core/src/main/java/cn/hutool/core/math/MathUtil.java b/hutool-core/src/main/java/cn/hutool/core/math/MathUtil.java new file mode 100644 index 000000000..9fab15912 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/math/MathUtil.java @@ -0,0 +1,79 @@ +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); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/math/package-info.java b/hutool-core/src/main/java/cn/hutool/core/math/package-info.java new file mode 100644 index 000000000..4d5784f49 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/net/LocalPortGenerater.java b/hutool-core/src/main/java/cn/hutool/core/net/LocalPortGenerater.java new file mode 100644 index 000000000..883d126cd --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/net/LocalPortGenerater.java @@ -0,0 +1,43 @@ +package cn.hutool.core.net; + +import java.io.Serializable; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * 本地端口生成器
+ * 用于生成本地可用(未被占用)的端口号
+ * 注意:多线程甚至单线程访问时可能会返回同一端口(例如获取了端口但是没有使用) + * + * @author looly + * @since 4.0.3 + * + */ +public class LocalPortGenerater implements Serializable{ + private static final long serialVersionUID = 1L; + + /** 备选的本地端口 */ + private final AtomicInteger alternativePort; + + /** + * 构造 + * + * @param beginPort + */ + public LocalPortGenerater(int beginPort) { + alternativePort = new AtomicInteger(beginPort); + } + + /** + * 生成一个本地端口,用于远程端口映射 + * + * @return 未被使用的本地端口 + */ + public int generate() { + int validPort = alternativePort.get(); + // 获取可用端口 + while (false == NetUtil.isUsableLocalPort(validPort)) { + validPort = alternativePort.incrementAndGet(); + } + return validPort; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/net/NetUtil.java b/hutool-core/src/main/java/cn/hutool/core/net/NetUtil.java new file mode 100644 index 000000000..0db55fa47 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/net/NetUtil.java @@ -0,0 +1,660 @@ +package cn.hutool.core.net; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.IDN; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.NetworkInterface; +import java.net.Socket; +import java.net.SocketException; +import java.net.URL; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.TreeSet; + +import javax.net.ServerSocketFactory; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +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.lang.Validator; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 网络相关工具 + * + * @author xiaoleilu + * + */ +public class NetUtil { + + public final static String LOCAL_IP = "127.0.0.1"; + + /** 默认最小端口,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 地址 + */ + public static String longToIpv4(long longIP) { + final StringBuilder sb = new StringBuilder(); + // 直接右移24位 + sb.append(String.valueOf(longIP >>> 24)); + sb.append("."); + // 将高8位置0,然后右移16位 + sb.append(String.valueOf((longIP & 0x00FFFFFF) >>> 16)); + sb.append("."); + sb.append(String.valueOf((longIP & 0x0000FFFF) >>> 8)); + sb.append("."); + sb.append(String.valueOf(longIP & 0x000000FF)); + return sb.toString(); + } + + /** + * 根据ip地址计算出long型的数据 + * + * @param strIP IP V4 地址 + * @return long值 + */ + public static long ipv4ToLong(String strIP) { + if (Validator.isIpv4(strIP)) { + long[] ip = new long[4]; + // 先找到IP地址字符串中.的位置 + int position1 = strIP.indexOf("."); + int position2 = strIP.indexOf(".", position1 + 1); + int position3 = strIP.indexOf(".", position2 + 1); + // 将每个.之间的字符串转换成整型 + ip[0] = Long.parseLong(strIP.substring(0, position1)); + ip[1] = Long.parseLong(strIP.substring(position1 + 1, position2)); + ip[2] = Long.parseLong(strIP.substring(position2 + 1, position3)); + ip[3] = Long.parseLong(strIP.substring(position3 + 1)); + return (ip[0] << 24) + (ip[1] << 16) + (ip[2] << 8) + ip[3]; + } + return 0; + } + + /** + * 检测本地端口可用性
+ * 来自org.springframework.util.SocketUtils + * + * @param port 被检测的端口 + * @return 是否可用 + */ + public static boolean isUsableLocalPort(int port) { + if (false == isValidPort(port)) { + // 给定的IP未在指定端口范围中 + return false; + } + try { + ServerSocketFactory.getDefault().createServerSocket(port, 1, InetAddress.getByName(LOCAL_IP)).close(); + return true; + } catch (Exception e) { + return false; + } + } + + /** + * 是否为有效的端口
+ * 此方法并不检查端口是否被占用 + * + * @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) { + for (int i = minPort; i <= maxPort; i++) { + if (isUsableLocalPort(RandomUtil.randomInt(minPort, maxPort + 1))) { + return i; + } + } + + throw new UtilException("Could not find an available port in the range [{}, {}] after {} attempts", minPort, maxPort, maxPort - minPort); + } + + /** + * 获取多个本地可用端口
+ * 来自org.springframework.util.SocketUtils + * + * @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; + } + + /** + * 判定是否为内网IP
+ * 私有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 + */ + public static boolean isInnerIP(String ipAddress) { + boolean isInnerIp = false; + long ipNum = NetUtil.ipv4ToLong(ipAddress); + + long aBegin = NetUtil.ipv4ToLong("10.0.0.0"); + long aEnd = NetUtil.ipv4ToLong("10.255.255.255"); + + long bBegin = NetUtil.ipv4ToLong("172.16.0.0"); + long bEnd = NetUtil.ipv4ToLong("172.31.255.255"); + + long cBegin = NetUtil.ipv4ToLong("192.168.0.0"); + long cEnd = NetUtil.ipv4ToLong("192.168.255.255"); + + isInnerIp = isInner(ipNum, aBegin, aEnd) || isInner(ipNum, bBegin, bEnd) || isInner(ipNum, cBegin, cEnd) || ipAddress.equals(LOCAL_IP); + return isInnerIp; + } + + /** + * 相对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 new StringBuffer(ip.length()).append(ip.substring(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 = null; + int port = 0; + 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; + } + } + + /** + * 获取本机所有网卡 + * + * @return 所有网卡,异常返回null + * @since 3.0.1 + */ + public static Collection getNetworkInterfaces() { + Enumeration networkInterfaces = null; + try { + networkInterfaces = NetworkInterface.getNetworkInterfaces(); + } catch (SocketException e) { + return null; + } + + return CollectionUtil.addAll(new ArrayList(), networkInterfaces); + } + + /** + * 获得本机的IPv4地址列表
+ * 返回的IP列表有序,按照系统设备顺序 + * + * @return IP地址列表 {@link LinkedHashSet} + */ + public static LinkedHashSet localIpv4s() { + final LinkedHashSet localAddressList = localAddressList(new Filter() { + + @Override + public boolean accept(InetAddress t) { + return t instanceof Inet4Address; + } + }); + + return toIpList(localAddressList); + } + + /** + * 获得本机的IPv6地址列表
+ * 返回的IP列表有序,按照系统设备顺序 + * + * @return IP地址列表 {@link LinkedHashSet} + * @since 4.5.17 + */ + public static LinkedHashSet localIpv6s() { + final LinkedHashSet localAddressList = localAddressList(new Filter() { + + @Override + public boolean accept(InetAddress t) { + return 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) { + Enumeration networkInterfaces = null; + 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(); + 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()}方法获取。
+ * 此方法不会抛出异常,获取失败将返回null
+ * + * 参考:http://stackoverflow.com/questions/9481865/getting-the-ip-address-of-the-current-machine-using-java + * + * @return 本机网卡IP地址,获取失败返回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()} 获取地址
+	 * 
+ * + * 此方法不会抛出异常,获取失败将返回null
+ * + * 见:https://github.com/looly/hutool/issues/428 + * + * @return 本机网卡IP地址,获取失败返回null + * @since 3.0.1 + */ + public static InetAddress getLocalhost() { + final LinkedHashSet localAddressList = localAddressList(new Filter() { + @Override + public boolean accept(InetAddress address) { + // 非loopback地址,指127.*.*.*的地址 + return false == address.isLoopbackAddress() + // 非地区本地地址,指10.0.0.0 ~ 10.255.255.255、172.16.0.0 ~ 172.31.255.255、192.168.0.0 ~ 192.168.255.255 + && false == address.isSiteLocalAddress() + // 需为IPV4地址 + && address instanceof Inet4Address; + } + }); + + if (CollUtil.isNotEmpty(localAddressList)) { + InetAddress address = CollUtil.get(localAddressList, 0); + return address; + } + + 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; + } + + byte[] mac; + try { + mac = NetworkInterface.getByInetAddress(inetAddress).getHardwareAddress(); + } catch (SocketException e) { + throw new UtilException(e); + } + 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; + } + + /** + * 创建 {@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 IOException 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) { + String[] ips = StrUtil.splitToArray(ip, '.'); + int ipAddr = (Integer.parseInt(ips[0]) << 24) | (Integer.parseInt(ips[1]) << 16) | (Integer.parseInt(ips[2]) << 8) | Integer.parseInt(ips[3]); + int type = Integer.parseInt(cidr.replaceAll(".*/", "")); + int mask = 0xFFFFFFFF << (32 - type); + String cidrIp = cidr.replaceAll("/.*", ""); + String[] cidrIps = cidrIp.split("\\."); + int cidrIpAddr = (Integer.parseInt(cidrIps[0]) << 24) | (Integer.parseInt(cidrIps[1]) << 16) | (Integer.parseInt(cidrIps[2]) << 8) | Integer.parseInt(cidrIps[3]); + return (ipAddr & 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 && ip.indexOf(",") > 0) { + final String[] ips = ip.trim().split(","); + for (String subIp : ips) { + if (false == isUnknow(subIp)) { + ip = subIp; + break; + } + } + } + return ip; + } + + /** + * 检测给定字符串是否为未知,多用于检测HTTP请求相关
+ * + * @param checkString 被检测的字符串 + * @return 是否未知 + * @since 4.4.1 + */ + public static boolean isUnknow(String checkString) { + return StrUtil.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString); + } + + // ----------------------------------------------------------------------------------------- Private method start + /** + * 指定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/hutool-core/src/main/java/cn/hutool/core/net/URLEncoder.java b/hutool-core/src/main/java/cn/hutool/core/net/URLEncoder.java new file mode 100644 index 000000000..5edef342b --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/net/URLEncoder.java @@ -0,0 +1,235 @@ +package cn.hutool.core.net; + +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; + +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.HexUtil; + +/** + * URL编码,数据内容的类型是 application/x-www-form-urlencoded。 + * + *
+ * 1.字符"a"-"z","A"-"Z","0"-"9",".","-","*",和"_" 都不会被编码;
+ * 2.将空格转换为%20 ;
+ * 3.将非文本内容转换成"%xy"的形式,xy是两位16进制的数值;
+ * 4.在每个 name=value 对之间放置 & 符号。
+ * 
+ * + * @author looly, + * + */ +public class URLEncoder implements Serializable{ + private static final long serialVersionUID = 1L; + + // --------------------------------------------------------------------------------------------- Static method start + /** + * 默认{@link URLEncoder}
+ * 默认的编码器针对URI路径编码,定义如下: + * + *
+	 * pchar = unreserved(不处理) / pct-encoded / sub-delims(子分隔符) / ":" / "@"
+	 * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
+	 * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
+	 * 
+ */ + public static final URLEncoder DEFAULT = createDefault(); + + /** + * 用于查询语句的{@link 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(); + + /** + * 创建默认{@link URLEncoder}
+ * 默认的编码器针对URI路径编码,定义如下: + * + *
+	 * pchar = unreserved(不处理) / pct-encoded / sub-delims(子分隔符) / ":" / "@"
+	 * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
+	 * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
+	 * 
+ * + * @return {@link URLEncoder} + */ + public static URLEncoder createDefault() { + final URLEncoder encoder = new URLEncoder(); + encoder.addSafeCharacter('-'); + encoder.addSafeCharacter('.'); + encoder.addSafeCharacter('_'); + encoder.addSafeCharacter('~'); + // 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('='); + // Add the remaining literals + encoder.addSafeCharacter(':'); + encoder.addSafeCharacter('@'); + // Add '/' so it isn't encoded when we encode a path + encoder.addSafeCharacter('/'); + + return encoder; + } + + /** + * 创建用于查询语句的{@link 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 {@link 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; + } + // --------------------------------------------------------------------------------------------- Static method end + + /** 存放安全编码 */ + private final BitSet safeCharacters; + /** 是否编码空格为+ */ + private boolean encodeSpaceAsPlus = false; + + /** + * 构造
+ * + * [a-zA-Z0-9]默认不被编码 + */ + public URLEncoder() { + this(new BitSet(256)); + + for (char i = 'a'; i <= 'z'; i++) { + addSafeCharacter(i); + } + for (char i = 'A'; i <= 'Z'; i++) { + addSafeCharacter(i); + } + for (char i = '0'; i <= '9'; i++) { + addSafeCharacter(i); + } + } + + /** + * 构造 + * + * @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 编码 + * + * @return 编码后的字符串 + */ + public String encode(String path, Charset charset) { + + int maxBytesPerChar = 10; + final StringBuilder rewrittenPath = new StringBuilder(path.length()); + ByteArrayOutputStream buf = new ByteArrayOutputStream(maxBytesPerChar); + 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 (int j = 0; j < ba.length; j++) { + // Converting each byte in the buffer + byte toEncode = ba[j]; + rewrittenPath.append('%'); + HexUtil.appendHex(rewrittenPath, toEncode, false); + } + buf.reset(); + } + } + return rewrittenPath.toString(); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/net/package-info.java b/hutool-core/src/main/java/cn/hutool/core/net/package-info.java new file mode 100644 index 000000000..6075b103c --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/package-info.java b/hutool-core/src/main/java/cn/hutool/core/package-info.java new file mode 100644 index 000000000..658283f77 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/swing/DesktopUtil.java b/hutool-core/src/main/java/cn/hutool/core/swing/DesktopUtil.java new file mode 100644 index 000000000..921fb31ee --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/swing/DesktopUtil.java @@ -0,0 +1,97 @@ +package cn.hutool.core.swing; + +import java.awt.Desktop; +import java.io.File; +import java.io.IOException; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.util.URLUtil; + +/** + * 桌面相关工具(平台相关)
+ * Desktop 类允许 Java 应用程序启动已在本机桌面上注册的关联应用程序,以处理 URI 或文件。 + * + * @author looly + * @since 4.5.7 + */ +public class DesktopUtil { + + /** + * 获得{@link Desktop} + * + * @return {@link Desktop} + */ + public static Desktop getDsktop() { + return Desktop.getDesktop(); + } + + /** + * 使用平台默认浏览器打开指定URL地址 + * + * @param url URL地址 + */ + public static void browse(String url) { + final Desktop dsktop = getDsktop(); + try { + dsktop.browse(URLUtil.toURI(url)); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 启动关联应用程序来打开文件 + * + * @param file URL地址 + */ + public static void open(File file) { + final Desktop dsktop = getDsktop(); + try { + dsktop.open(file); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 启动关联编辑器应用程序并打开用于编辑的文件 + * + * @param file 文件 + */ + public static void edit(File file) { + final Desktop dsktop = getDsktop(); + try { + dsktop.edit(file); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 使用关联应用程序的打印命令, 用本机桌面打印设备来打印文件 + * + * @param file 文件 + */ + public static void print(File file) { + final Desktop dsktop = getDsktop(); + try { + dsktop.print(file); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 使用平台默认浏览器打开指定URL地址 + * + * @param mailAddress 邮件地址 + */ + public static void mail(String mailAddress) { + final Desktop dsktop = getDsktop(); + try { + dsktop.mail(URLUtil.toURI(mailAddress)); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/swing/RobotUtil.java b/hutool-core/src/main/java/cn/hutool/core/swing/RobotUtil.java new file mode 100644 index 000000000..64abd532b --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/swing/RobotUtil.java @@ -0,0 +1,206 @@ +package cn.hutool.core.swing; + +import java.awt.AWTException; +import java.awt.Rectangle; +import java.awt.Robot; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; +import java.awt.image.BufferedImage; +import java.io.File; + +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.img.ImgUtil; +import cn.hutool.core.swing.clipboard.ClipboardUtil; + +/** + * {@link Robot} 封装工具类,提供截屏等工具 + * + * @author looly + * @since 4.1.14 + */ +public class RobotUtil { + + private static final Robot robot; + private static int delay; + + static { + try { + robot = new Robot(); + } catch (AWTException e) { + throw new UtilException(e); + } + } + + /** + * 设置默认的延迟时间
+ * 当按键执行完后的等待时间,也可以用ThreadUtil.sleep方法代替 + * + * @param delayMillis 等待毫秒数 + * @since 4.5.7 + */ + public static void setDelay(int delayMillis) { + delay = delayMillis; + } + + /** + * 模拟鼠标移动 + * + * @param x 移动到的x坐标 + * @param y 移动到的y坐标 + * @since 4.5.7 + */ + public static void mouseMove(int x, int y) { + robot.mouseMove(x, y); + } + + /** + * 模拟单击
+ * 鼠标单击包括鼠标左键的按下和释放 + * + * @since 4.5.7 + */ + public static void click() { + robot.mousePress(InputEvent.BUTTON1_MASK); + robot.mouseRelease(InputEvent.BUTTON1_MASK); + delay(); + } + + /** + * 模拟右键单击
+ * 鼠标单击包括鼠标右键的按下和释放 + * + * @since 4.5.7 + */ + public static void rightClick() { + robot.mousePress(InputEvent.BUTTON1_MASK); + robot.mouseRelease(InputEvent.BUTTON1_MASK); + delay(); + } + + /** + * 模拟鼠标滚轮滚动 + * + * @param wheelAmt 滚动数,负数表示向前滚动,正数向后滚动 + * @since 4.5.7 + */ + public static void mouseWheel(int wheelAmt) { + robot.mouseWheel(wheelAmt); + delay(); + } + + /** + * 模拟键盘点击
+ * 包括键盘的按下和释放 + * + * @param keyCodes 按键码列表,见{@link java.awt.event.KeyEvent} + * @since 4.5.7 + */ + public static void keyClick(int... keyCodes) { + for (int keyCode : keyCodes) { + robot.keyPress(keyCode); + robot.keyRelease(keyCode); + } + delay(); + } + + /** + * 打印输出指定字符串(借助剪贴板) + * + * @param str 字符串 + */ + public static void keyPressString(String str) { + ClipboardUtil.setStr(str); + keyPressWithCtrl(KeyEvent.VK_V);// 粘贴 + delay(); + } + + /** + * shift+ 按键 + * + * @param key 按键 + */ + public static void keyPressWithShift(int key) { + robot.keyPress(KeyEvent.VK_SHIFT); + robot.keyPress(key); + robot.keyRelease(key); + robot.keyRelease(KeyEvent.VK_SHIFT); + delay(); + } + + /** + * ctrl+ 按键 + * + * @param key 按键 + */ + public static void keyPressWithCtrl(int key) { + robot.keyPress(KeyEvent.VK_CONTROL); + robot.keyPress(key); + robot.keyRelease(key); + robot.keyRelease(KeyEvent.VK_CONTROL); + delay(); + } + + /** + * alt+ 按键 + * + * @param key 按键 + */ + public static void keyPressWithAlt(int key) { + robot.keyPress(KeyEvent.VK_ALT); + robot.keyPress(key); + robot.keyRelease(key); + robot.keyRelease(KeyEvent.VK_ALT); + delay(); + } + + /** + * 截取全屏 + * + * @return 截屏的图片 + */ + public static BufferedImage captureScreen() { + return captureScreen(ScreenUtil.getRectangle()); + } + + /** + * 截取全屏到文件 + * + * @param outFile 写出到的文件 + * @return 写出到的文件 + */ + public static File captureScreen(File outFile) { + ImgUtil.write(captureScreen(), outFile); + return outFile; + } + + /** + * 截屏 + * + * @param screenRect 截屏的矩形区域 + * @return 截屏的图片 + */ + public static BufferedImage captureScreen(Rectangle screenRect) { + return robot.createScreenCapture(screenRect); + } + + /** + * 截屏 + * + * @param screenRect 截屏的矩形区域 + * @param outFile 写出到的文件 + * @return 写出到的文件 + */ + public static File captureScreen(Rectangle screenRect, File outFile) { + ImgUtil.write(captureScreen(screenRect), outFile); + return outFile; + } + + /** + * 等待指定毫秒数 + */ + private static void delay() { + if (delay > 0) { + robot.delay(delay); + } + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/swing/ScreenUtil.java b/hutool-core/src/main/java/cn/hutool/core/swing/ScreenUtil.java new file mode 100644 index 000000000..64dba86d0 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/swing/ScreenUtil.java @@ -0,0 +1,88 @@ +package cn.hutool.core.swing; + +import java.awt.Dimension; +import java.awt.Rectangle; +import java.awt.Toolkit; +import java.awt.image.BufferedImage; +import java.io.File; + +/** + * 屏幕相关(当前显示设置)工具类 + * + * @author looly + * @since 4.1.14 + */ +public class ScreenUtil { + public static Dimension dimension = Toolkit.getDefaultToolkit().getScreenSize(); + + /** + * 获取屏幕宽度 + * + * @return 屏幕宽度 + */ + public static int getWidth() { + return (int) dimension.getWidth(); + } + + /** + * 获取屏幕高度 + * + * @return 屏幕高度 + */ + public static int getHeight() { + return (int) dimension.getHeight(); + } + + /** + * 获取屏幕的矩形 + * @return 屏幕的矩形 + */ + public static Rectangle getRectangle() { + return new Rectangle(getWidth(), getHeight()); + } + + //-------------------------------------------------------------------------------------------- 截屏 + /** + * 截取全屏 + * + * @return 截屏的图片 + * @see RobotUtil#captureScreen() + */ + public static BufferedImage captureScreen() { + return RobotUtil.captureScreen(); + } + + /** + * 截取全屏到文件 + * + * @param outFile 写出到的文件 + * @return 写出到的文件 + * @see RobotUtil#captureScreen(File) + */ + public static File captureScreen(File outFile) { + return RobotUtil.captureScreen(outFile); + } + + /** + * 截屏 + * + * @param screenRect 截屏的矩形区域 + * @return 截屏的图片 + * @see RobotUtil#captureScreen(Rectangle) + */ + public static BufferedImage captureScreen(Rectangle screenRect) { + return RobotUtil.captureScreen(screenRect); + } + + /** + * 截屏 + * + * @param screenRect 截屏的矩形区域 + * @param outFile 写出到的文件 + * @return 写出到的文件 + * @see RobotUtil#captureScreen(Rectangle, File) + */ + public static File captureScreen(Rectangle screenRect, File outFile) { + return RobotUtil.captureScreen(screenRect, outFile); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/swing/clipboard/ClipboardListener.java b/hutool-core/src/main/java/cn/hutool/core/swing/clipboard/ClipboardListener.java new file mode 100644 index 000000000..091703606 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/swing/clipboard/ClipboardListener.java @@ -0,0 +1,23 @@ +package cn.hutool.core.swing.clipboard; + +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.Transferable; + +/** + * 剪贴板监听事件处理接口
+ * 用户通过实现此接口,实现监听剪贴板内容变化 + * + * @author looly + *@since 4.5.6 + */ +public interface ClipboardListener { + /** + * 剪贴板变动触发的事件方法
+ * 在此事件中对剪贴板设置值无效,如若修改,需返回修改内容 + * + * @param clipboard 剪贴板对象 + * @param contents 内容 + * @return 如果对剪贴板内容做修改,则返回修改的内容,{@code null}表示保留原内容 + */ + Transferable onChange(Clipboard clipboard, Transferable contents); +} diff --git a/hutool-core/src/main/java/cn/hutool/core/swing/clipboard/ClipboardMonitor.java b/hutool-core/src/main/java/cn/hutool/core/swing/clipboard/ClipboardMonitor.java new file mode 100644 index 000000000..b14d71d09 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/swing/clipboard/ClipboardMonitor.java @@ -0,0 +1,207 @@ +package cn.hutool.core.swing.clipboard; + +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.ClipboardOwner; +import java.awt.datatransfer.Transferable; +import java.io.Closeable; +import java.util.LinkedHashSet; +import java.util.Set; + +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.core.util.ObjectUtil; + +/** + * 剪贴板监听 + * + * @author looly + * @since 4.5.6 + */ +public enum ClipboardMonitor implements ClipboardOwner, Runnable, Closeable { + INSTANCE; + + /** 默认重试此时:10 */ + public static final int DEFAULT_TRY_COUNT = 10; + /** 默认重试等待:100 */ + public static final long DEFAULT_DELAY = 100; + + /** 重试次数 */ + private int tryCount; + /** 重试等待 */ + private long delay; + /** 系统剪贴板对象 */ + private Clipboard clipboard; + /** 监听事件处理 */ + private Set listenerSet = new LinkedHashSet<>(); + /** 是否正在监听 */ + private boolean isRunning; + + // ---------------------------------------------------------------------------------------------------------- Constructor start + /** + * 构造,尝试获取剪贴板内容的次数为10,第二次之后延迟100毫秒 + */ + private ClipboardMonitor() { + this(DEFAULT_TRY_COUNT, DEFAULT_DELAY); + } + + /** + * 构造 + * + * @param tryCount 尝试获取剪贴板内容的次数 + * @param delay 响应延迟,当从第二次开始,延迟一定毫秒数等待剪贴板可以获取,当tryCount小于2时无效 + */ + private ClipboardMonitor(int tryCount, long delay) { + this(tryCount, delay, ClipboardUtil.getClipboard()); + } + + /** + * 构造 + * + * @param tryCount 尝试获取剪贴板内容的次数 + * @param delay 响应延迟,当从第二次开始,延迟一定毫秒数等待剪贴板可以获取,当tryCount小于2时无效 + * @param clipboard 剪贴板对象 + */ + private ClipboardMonitor(int tryCount, long delay, Clipboard clipboard) { + this.tryCount = tryCount; + this.delay = delay; + this.clipboard = clipboard; + } + // ---------------------------------------------------------------------------------------------------------- Constructor end + + /** + * 设置重试次数 + * + * @param tryCount 重试次数 + * @return this + */ + public ClipboardMonitor setTryCount(int tryCount) { + this.tryCount = tryCount; + return this; + } + + /** + * 设置重试等待 + * + * @param delay 重试等待 + * @return this + */ + public ClipboardMonitor setDelay(long delay) { + this.delay = delay; + return this; + } + + /** + * 设置 监听事件处理 + * + * @param listener 监听事件处理 + * @return this + */ + public ClipboardMonitor addListener(ClipboardListener listener) { + this.listenerSet.add(listener); + return this; + } + + /** + * 去除指定监听 + * + * @param listener 监听 + * @return this + */ + public ClipboardMonitor removeListener(ClipboardListener listener) { + this.listenerSet.remove(listener); + return this; + } + + /** + * 清空监听 + * + * @return this + */ + public ClipboardMonitor clearListener() { + this.listenerSet.clear(); + return this; + } + + @Override + public void lostOwnership(Clipboard clipboard, Transferable contents) { + Transferable newContents; + try { + newContents = tryGetContent(clipboard); + } catch (InterruptedException e) { + // 中断后结束简体 + return; + } + + Transferable transferable = null; + for (ClipboardListener listener : listenerSet) { + try { + transferable = listener.onChange(clipboard, ObjectUtil.defaultIfNull(transferable, newContents)); + } catch (Throwable e) { + // 忽略事件处理异常,保证所有监听正常执行 + } + } + + if (isRunning) { + // 继续监听 + clipboard.setContents(ObjectUtil.defaultIfNull(transferable, ObjectUtil.defaultIfNull(newContents, contents)), this); + } + } + + @Override + public synchronized void run() { + if(false == isRunning) { + final Clipboard clipboard = this.clipboard; + clipboard.setContents(clipboard.getContents(null), this); + isRunning = true; + } + } + + /** + * 开始监听 + * + * @param sync 是否阻塞 + */ + public void listen(boolean sync) { + run(); + + if (sync) { + ThreadUtil.sync(this); + } + } + + /** + * 关闭(停止)监听 + */ + @Override + public void close() { + this.isRunning = false; + } + + // ------------------------------------------------------------------------------------------------------------------------- Private method start + /** + * 尝试获取剪贴板内容 + * + * @param clipboard 剪贴板 + * @return 剪贴板内容,{@code null} 表示未获取到 + * @throws InterruptedException 线程中断 + */ + private Transferable tryGetContent(Clipboard clipboard) throws InterruptedException { + Transferable newContents = null; + for (int i = 0; i < this.tryCount; i++) { + if (this.delay > 0 && i > 0) { + // 第一次获取不等待,只有从第二次获取时才开始等待 + Thread.sleep(this.delay); + } + + try { + newContents = clipboard.getContents(null); + } catch (IllegalStateException e) { + // ignore + } + if (null != newContents) { + return newContents; + } + } + return newContents; + } + // ------------------------------------------------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-core/src/main/java/cn/hutool/core/swing/clipboard/ClipboardUtil.java b/hutool-core/src/main/java/cn/hutool/core/swing/clipboard/ClipboardUtil.java new file mode 100644 index 000000000..6dfaf0e25 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/swing/clipboard/ClipboardUtil.java @@ -0,0 +1,177 @@ +package cn.hutool.core.swing.clipboard; + +import java.awt.Image; +import java.awt.Toolkit; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.ClipboardOwner; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.StringSelection; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.io.IOException; + +import cn.hutool.core.exceptions.UtilException; + +/** + * 系统剪贴板工具类 + * + * @author looly + * @since 3.2.0 + */ +public class ClipboardUtil { + + /** + * 获取系统剪贴板 + * + * @return {@link Clipboard} + */ + public static Clipboard getClipboard() { + return Toolkit.getDefaultToolkit().getSystemClipboard(); + } + + /** + * 设置内容到剪贴板 + * + * @param contents 内容 + */ + public static void set(Transferable contents) { + set(contents, null); + } + + /** + * 设置内容到剪贴板 + * + * @param contents 内容 + * @param owner 所有者 + */ + public static void set(Transferable contents, ClipboardOwner owner) { + getClipboard().setContents(contents, owner); + } + + /** + * 获取剪贴板内容 + * + * @param flavor 数据元信息,标识数据类型 + * @return 剪贴板内容,类型根据flavor不同而不同 + */ + public static Object get(DataFlavor flavor) { + return get(getClipboard().getContents(null), flavor); + } + + /** + * 获取剪贴板内容 + * + * @param content {@link Transferable} + * @param flavor 数据元信息,标识数据类型 + * @return 剪贴板内容,类型根据flavor不同而不同 + */ + public static Object get(Transferable content, DataFlavor flavor) { + if (null != content && content.isDataFlavorSupported(flavor)) { + try { + return content.getTransferData(flavor); + } catch (UnsupportedFlavorException | IOException e) { + throw new UtilException(e); + } + } + return null; + } + + /** + * 设置字符串文本到剪贴板 + * + * @param text 字符串文本 + */ + public static void setStr(String text) { + set(new StringSelection(text)); + } + + /** + * 从剪贴板获取文本 + * + * @return 文本 + */ + public static String getStr() { + return (String) get(DataFlavor.stringFlavor); + } + + /** + * 从剪贴板的{@link Transferable}获取文本 + * + * @param content + * @return 文本 + * @since 4.5.6 + */ + public static String getStr(Transferable content) { + return (String) get(content, DataFlavor.stringFlavor); + } + + /** + * 设置图片到剪贴板 + * + * @param image 图像 + */ + public static void setImage(Image image) { + set(new ImageSelection(image), null); + } + + /** + * 从剪贴板获取图片 + * + * @return 图片{@link Image} + */ + public static Image getImage() { + return (Image) get(DataFlavor.imageFlavor); + } + + /** + * 从剪贴板的{@link Transferable}获取图片 + * + * @param content + * @return 图片 + * @since 4.5.6 + */ + public static Image getImage(Transferable content) { + return (Image) get(content, DataFlavor.imageFlavor); + } + + /** + * 监听剪贴板修改事件 + * + * @param listener 监听处理接口 + * @since 4.5.6 + * @see ClipboardMonitor#listen(boolean) + */ + public static void listen(ClipboardListener listener) { + listen(listener, true); + } + + /** + * 监听剪贴板修改事件 + * + * @param listener 监听处理接口 + * @param sync 是否同步阻塞 + * @since 4.5.6 + * @see ClipboardMonitor#listen(boolean) + */ + public static void listen(ClipboardListener listener, boolean sync) { + listen(ClipboardMonitor.DEFAULT_TRY_COUNT, ClipboardMonitor.DEFAULT_DELAY, listener, sync); + } + + /** + * 监听剪贴板修改事件 + * + * @param tryCount 尝试获取剪贴板内容的次数 + * @param delay 响应延迟,当从第二次开始,延迟一定毫秒数等待剪贴板可以获取 + * @param listener 监听处理接口 + * @param sync 是否同步阻塞 + * @since 4.5.6 + * @see ClipboardMonitor#listen(boolean) + */ + public static void listen(int tryCount, long delay, ClipboardListener listener, boolean sync) { + ClipboardMonitor.INSTANCE// + .setTryCount(tryCount)// + .setDelay(delay)// + .addListener(listener)// + .listen(sync); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/swing/clipboard/ImageSelection.java b/hutool-core/src/main/java/cn/hutool/core/swing/clipboard/ImageSelection.java new file mode 100644 index 000000000..21380df85 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/swing/clipboard/ImageSelection.java @@ -0,0 +1,65 @@ +package cn.hutool.core.swing.clipboard; + +import java.awt.Image; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.io.IOException; +import java.io.Serializable; + +/** + * 图片转换器,用于将图片对象转换为剪贴板支持的对象
+ * 此对象也用于将图像文件和{@link DataFlavor#imageFlavor} 元信息对应 + * + * @author looly + * @since 4.5.6 + */ +public class ImageSelection implements Transferable, Serializable { + private static final long serialVersionUID = 1L; + + private Image image; + + /** + * 构造 + * + * @param image 图片 + */ + public ImageSelection(Image image) { + this.image = image; + } + + /** + * 获取元数据类型信息 + * + * @return 元数据类型列表 + */ + @Override + public DataFlavor[] getTransferDataFlavors() { + return new DataFlavor[] { DataFlavor.imageFlavor }; + } + + /** + * 是否支持指定元数据类型 + * + * @param flavor 元数据类型 + * @return 是否支持 + */ + @Override + public boolean isDataFlavorSupported(DataFlavor flavor) { + return DataFlavor.imageFlavor.equals(flavor); + } + + /** + * 获取图片 + * + * @param flavor 元数据类型 + * @return 转换后的对象 + */ + @Override + public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException { + if (false == DataFlavor.imageFlavor.equals(flavor)) { + throw new UnsupportedFlavorException(flavor); + } + return image; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/swing/clipboard/StrClipboardListener.java b/hutool-core/src/main/java/cn/hutool/core/swing/clipboard/StrClipboardListener.java new file mode 100644 index 000000000..cd6c7e3e5 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/swing/clipboard/StrClipboardListener.java @@ -0,0 +1,34 @@ +package cn.hutool.core.swing.clipboard; + +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.io.Serializable; + +/** + * 剪贴板字符串内容监听 + * + * @author looly + * @since 4.5.7 + */ +public abstract class StrClipboardListener implements ClipboardListener, Serializable { + private static final long serialVersionUID = 1L; + + @Override + public Transferable onChange(Clipboard clipboard, Transferable contents) { + if (contents.isDataFlavorSupported(DataFlavor.stringFlavor)) { + return onChange(clipboard, ClipboardUtil.getStr(contents)); + } + return null; + } + + /** + * 剪贴板变动触发的事件方法
+ * 在此事件中对剪贴板设置值无效,如若修改,需返回修改内容 + * + * @param clipboard 剪贴板对象 + * @param contents 内容 + * @return 如果对剪贴板内容做修改,则返回修改的内容,{@code null}表示保留原内容 + */ + public abstract Transferable onChange(Clipboard clipboard, String contents); +} diff --git a/hutool-core/src/main/java/cn/hutool/core/swing/clipboard/package-info.java b/hutool-core/src/main/java/cn/hutool/core/swing/clipboard/package-info.java new file mode 100644 index 000000000..b8b155eb5 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/swing/clipboard/package-info.java @@ -0,0 +1,7 @@ +/** + * 剪贴板相关的工具,包括剪贴板监听等 + * + * @author looly + * + */ +package cn.hutool.core.swing.clipboard; \ No newline at end of file diff --git a/hutool-core/src/main/java/cn/hutool/core/swing/package-info.java b/hutool-core/src/main/java/cn/hutool/core/swing/package-info.java new file mode 100644 index 000000000..dfbee02c5 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/swing/package-info.java @@ -0,0 +1,7 @@ +/** + * Swing和awt相关封装 + * + * @author looly + * + */ +package cn.hutool.core.swing; \ No newline at end of file diff --git a/hutool-core/src/main/java/cn/hutool/core/text/ASCIIStrCache.java b/hutool-core/src/main/java/cn/hutool/core/text/ASCIIStrCache.java new file mode 100644 index 000000000..17fd8828b --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/text/Simhash.java b/hutool-core/src/main/java/cn/hutool/core/text/Simhash.java new file mode 100644 index 000000000..8318e2e42 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/text/Simhash.java @@ -0,0 +1,199 @@ +package cn.hutool.core.text; + +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.ReentrantReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; +import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; + +import cn.hutool.core.lang.MurmurHash; + +/** + *

+ * 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 List>> storage; + private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + + /** + * 构造 + */ + 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 ReadLock readLock = this.lock.readLock(); + readLock.lock(); + 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 { + readLock.unlock(); + } + return false; + } + + /** + * 按照(frac, )索引进行存储 + * + * @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 WriteLock writeLock = this.lock.writeLock(); + writeLock.lock(); + 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 { + writeLock.unlock(); + } + } + + //------------------------------------------------------------------------------------------------------ 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/hutool-core/src/main/java/cn/hutool/core/text/StrBuilder.java b/hutool-core/src/main/java/cn/hutool/core/text/StrBuilder.java new file mode 100644 index 000000000..2cfd1e2cc --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/text/StrBuilder.java @@ -0,0 +1,539 @@ +package cn.hutool.core.text; + +import java.io.Serializable; +import java.util.Arrays; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 可复用的字符串生成器,非线程安全 + * + * @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 {@link StrBuilder} + */ + public static StrBuilder create() { + return new StrBuilder(); + } + + /** + * 创建字符串构建器 + * @param initialCapacity 初始容量 + * @return {@link StrBuilder} + */ + public static StrBuilder create(int initialCapacity) { + return new StrBuilder(initialCapacity); + } + + /** + * 创建字符串构建器 + * @param strs 初始字符串 + * @return {@link StrBuilder} + * @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(int i = 0; i < strs.length; i++) { + append(strs[i]); + } + } + // ------------------------------------------------------------------------------------ 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 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) { + 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 = 0; + } + 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 (null == csq) { + csq = "null"; + } + 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 = 0; + } + + 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) { + this.reset(); + } else if (newPosition < this.position) { + this.position = newPosition; + } + return this; + } + + /** + * 删除指定长度的字符 + * + * @param start 开始位置(包括) + * @param end 结束位置(不包括) + * @return this + */ + public StrBuilder del(int start, int end) { + if (start < 0) { + start = 0; + } + if (end > this.position) { + end = this.position; + } + if (start > end) { + throw new StringIndexOutOfBoundsException("Start is greater than End."); + } + if (end == this.position) { + this.position = start; + } + + int len = end - start; + if (len > 0) { + System.arraycopy(value, start + len, value, start, this.position - end); + this.position -= len; + } + 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)) { + 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) { + if (minimumCapacity > value.length) { + expandCapacity(minimumCapacity); + } + } + + /** + * 扩展容量
+ * 首先对容量进行二倍扩展,如果小于最小容量,则扩展为最小容量 + * + * @param minimumCapacity 需要扩展的最小容量 + */ + private void expandCapacity(int minimumCapacity) { + int newCapacity = value.length * 2 + 2; + if (newCapacity < minimumCapacity) { + newCapacity = minimumCapacity; + } + if (newCapacity < 0) { + if (minimumCapacity < 0) { + // overflow + throw new OutOfMemoryError("Capacity is too long and max than Integer.MAX"); + } + newCapacity = Integer.MAX_VALUE; + } + value = Arrays.copyOf(value, newCapacity); + } + + /** + * 给定字符串数组的总长度
+ * null字符长度定义为0 + * + * @param strs 字符串数组 + * @return 总长度 + * @since 4.0.1 + */ + private static int totalLength(CharSequence... strs) { + int totalLength = 0; + for(int i = 0 ; i < strs.length; i++) { + totalLength += (null == strs[i] ? 4 : strs[i].length()); + } + return totalLength; + } + // ------------------------------------------------------------------------------------ Private method end +} diff --git a/hutool-core/src/main/java/cn/hutool/core/text/StrFormatter.java b/hutool-core/src/main/java/cn/hutool/core/text/StrFormatter.java new file mode 100644 index 000000000..03a87e59c --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/text/StrFormatter.java @@ -0,0 +1,76 @@ +package cn.hutool.core.text; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 字符串格式化工具 + * + * @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(final String strPattern, final Object... argArray) { + if (StrUtil.isBlank(strPattern) || ArrayUtil.isEmpty(argArray)) { + return strPattern; + } + final int strPatternLength = strPattern.length(); + + // 初始化定义好的长度以获得更好的性能 + StringBuilder sbuf = new StringBuilder(strPatternLength + 50); + + int handledPosition = 0;// 记录已经处理到的位置 + int delimIndex;// 占位符所在位置 + for (int argIndex = 0; argIndex < argArray.length; argIndex++) { + delimIndex = strPattern.indexOf(StrUtil.EMPTY_JSON, 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 + 2; + } else { + // 占位符被转义 + argIndex--; + sbuf.append(strPattern, handledPosition, delimIndex - 1); + sbuf.append(StrUtil.C_DELIM_START); + handledPosition = delimIndex + 1; + } + } else {// 正常占位符 + sbuf.append(strPattern, handledPosition, delimIndex); + sbuf.append(StrUtil.utf8Str(argArray[argIndex])); + handledPosition = delimIndex + 2; + } + } + + // append the characters following the last {} pair. + // 加入最后一个占位符后所有的字符 + sbuf.append(strPattern, handledPosition, strPattern.length()); + + return sbuf.toString(); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/text/StrSpliter.java b/hutool-core/src/main/java/cn/hutool/core/text/StrSpliter.java new file mode 100644 index 000000000..cccafb1fc --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/text/StrSpliter.java @@ -0,0 +1,513 @@ +package cn.hutool.core.text; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import cn.hutool.core.lang.PatternPool; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 字符串切分器 + * @author Looly + * + */ +public class StrSpliter { + + //---------------------------------------------------------------------------------------------- Split by char + /** + * 切分字符串路径,仅支持Unix分界符:/ + * + * @param str 被切分的字符串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List splitPath(String str){ + return splitPath(str, 0); + } + + /** + * 切分字符串路径,仅支持Unix分界符:/ + * + * @param str 被切分的字符串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static String[] splitPathToArray(String str){ + return toArray(splitPath(str)); + } + + /** + * 切分字符串路径,仅支持Unix分界符:/ + * + * @param str 被切分的字符串 + * @param limit 限制分片数 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List splitPath(String 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(String str, int limit){ + return toArray(splitPath(str, limit)); + } + + /** + * 切分字符串 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.2.1 + */ + public static List splitTrim(String 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(String 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(String 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(String str, char separator, int limit, boolean isTrim, boolean ignoreEmpty){ + return split(str, separator, limit, isTrim, ignoreEmpty, false); + } + + /** + * 切分字符串,忽略大小写 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.2.1 + */ + public static List splitIgnoreCase(String str, char separator, int limit, boolean isTrim, boolean ignoreEmpty){ + return split(str, separator, limit, isTrim, ignoreEmpty, true); + } + + /** + * 切分字符串 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @param ignoreCase 是否忽略大小写 + * @return 切分后的集合 + * @since 3.2.1 + */ + public static List split(String str, char separator, int limit, boolean isTrim, boolean ignoreEmpty, boolean ignoreCase){ + if(StrUtil.isEmpty(str)){ + return new ArrayList(0); + } + if(limit == 1){ + return addToList(new ArrayList(1), str, isTrim, ignoreEmpty); + } + + final ArrayList list = new ArrayList<>(limit > 0 ? limit : 16); + int len = str.length(); + int start = 0;//切分后每个部分的起始 + for(int i = 0; i < len; i++){ + if(NumberUtil.equals(separator, str.charAt(i), ignoreCase)){ + addToList(list, str.substring(start, i), isTrim, ignoreEmpty); + start = i+1;//i+1同时将start与i保持一致 + + //检查是否超出范围(最大允许limit-1个,剩下一个留给末尾字符串) + if(limit > 0 && list.size() > limit-2){ + break; + } + } + } + return addToList(list, str.substring(start, len), isTrim, ignoreEmpty);//收尾 + } + + /** + * 切分字符串为字符串数组 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static String[] splitToArray(String 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(String 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(String str, String separator, boolean ignoreEmpty){ + return split(str, separator, true, ignoreEmpty); + } + + /** + * 切分字符串,不忽略大小写 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符串 + * @param limit 限制分片数 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List split(String 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(String 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(String 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(String str, String separator, int limit, boolean ignoreEmpty){ + return split(str, separator, limit, true, ignoreEmpty, true); + } + + /** + * 切分字符串 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符串 + * @param limit 限制分片数 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @param ignoreCase 是否忽略大小写 + * @return 切分后的集合 + * @since 3.2.1 + */ + public static List split(String str, String separator, int limit, boolean isTrim, boolean ignoreEmpty, boolean ignoreCase){ + if(StrUtil.isEmpty(str)){ + return new ArrayList(0); + } + if(limit == 1){ + return addToList(new ArrayList(1), str, isTrim, ignoreEmpty); + } + + if(StrUtil.isEmpty(separator)){//分隔符为空时按照空白符切分 + return split(str, limit); + }else if(separator.length() == 1){//分隔符只有一个字符长度时按照单分隔符切分 + return split(str, separator.charAt(0), limit, isTrim, ignoreEmpty, ignoreCase); + } + + final ArrayList list = new ArrayList<>(); + int len = str.length(); + int separatorLen = separator.length(); + int start = 0; + int i = 0; + while(i < len){ + i = StrUtil.indexOf(str, separator, start, ignoreCase); + if(i > -1){ + addToList(list, str.substring(start, i), isTrim, ignoreEmpty); + start = i + separatorLen; + + //检查是否超出范围(最大允许limit-1个,剩下一个留给末尾字符串) + if(limit > 0 && list.size() > limit-2){ + break; + } + }else{ + break; + } + } + return addToList(list, str.substring(start, len), isTrim, ignoreEmpty); + } + + /** + * 切分字符串为字符串数组 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static String[] splitToArray(String str, String separator, int limit, boolean isTrim, boolean ignoreEmpty){ + return toArray(split(str, separator, limit, isTrim, ignoreEmpty)); + } + + //---------------------------------------------------------------------------------------------- Split by Whitespace + + /** + * 使用空白符切分字符串
+ * 切分后的字符串两边不包含空白符,空串或空白符串并不做为元素之一 + * + * @param str 被切分的字符串 + * @param limit 限制分片数 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List split(String str, int limit){ + if(StrUtil.isEmpty(str)){ + return new ArrayList(0); + } + if(limit == 1){ + return addToList(new ArrayList(1), str, true, true); + } + + final ArrayList list = new ArrayList<>(); + int len = str.length(); + int start = 0;//切分后每个部分的起始 + for(int i = 0; i < len; i++){ + if(CharUtil.isBlankChar(str.charAt(i))){ + addToList(list, str.substring(start, i), true, true); + start = i+1;//i+1同时将start与i保持一致 + + //检查是否超出范围(最大允许limit-1个,剩下一个留给末尾字符串) + if(limit > 0 && list.size() > limit-2){ + break; + } + } + } + return addToList(list, str.substring(start, len), true, true);//收尾 + } + + /** + * 切分字符串为字符串数组 + * + * @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 str 字符串 + * @param separatorRegex 分隔符正则 + * @param limit 限制分片数 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List splitByRegex(String str, String separatorRegex, int limit, boolean isTrim, boolean ignoreEmpty){ + final Pattern pattern = PatternPool.get(separatorRegex); + return split(str, pattern, limit, isTrim, ignoreEmpty); + } + + /** + * 通过正则切分字符串 + * @param str 字符串 + * @param separatorPattern 分隔符正则{@link Pattern} + * @param limit 限制分片数 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List split(String str, Pattern separatorPattern, int limit, boolean isTrim, boolean ignoreEmpty){ + if(StrUtil.isEmpty(str)){ + return new ArrayList(0); + } + if(limit == 1){ + return addToList(new ArrayList(1), str, isTrim, ignoreEmpty); + } + + if(null == separatorPattern){//分隔符为空时按照空白符切分 + return split(str, limit); + } + + final Matcher matcher = separatorPattern.matcher(str); + final ArrayList list = new ArrayList<>(); + int len = str.length(); + int start = 0; + while(matcher.find()){ + addToList(list, str.substring(start, matcher.start()), isTrim, ignoreEmpty); + start = matcher.end(); + + //检查是否超出范围(最大允许limit-1个,剩下一个留给末尾字符串) + if(limit > 0 && list.size() > limit-2){ + break; + } + } + return addToList(list, str.substring(start, len), isTrim, ignoreEmpty); + } + + /** + * 通过正则切分字符串为字符串数组 + * + * @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 str 字符串 + * @param len 每一个小节的长度 + * @return 截取后的字符串数组 + */ + public static String[] splitByLength(String str, int len) { + int partCount = str.length() / len; + int lastPartCount = str.length() % len; + int fixPart = 0; + if (lastPartCount != 0) { + fixPart = 1; + } + + final String[] strs = new String[partCount + fixPart]; + for (int i = 0; i < partCount + fixPart; i++) { + if (i == partCount + fixPart - 1 && lastPartCount != 0) { + strs[i] = str.substring(i * len, i * len + lastPartCount); + } else { + strs[i] = str.substring(i * len, i * len + len); + } + } + return strs; + } + + //---------------------------------------------------------------------------------------------------------- Private method start + /** + * 将字符串加入List中 + * @param list 列表 + * @param part 被加入的部分 + * @param isTrim 是否去除两端空白符 + * @param ignoreEmpty 是否略过空字符串(空字符串不做为一个元素) + * @return 列表 + */ + private static List addToList(List list, String part, boolean isTrim, boolean ignoreEmpty){ + if(isTrim){ + part = StrUtil.trim(part); + } + if(false == ignoreEmpty || false == part.isEmpty()){ + list.add(part); + } + return list; + } + + /** + * List转Array + * @param list List + * @return Array + */ + private static String[] toArray(List list){ + return list.toArray(new String[list.size()]); + } + //---------------------------------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-core/src/main/java/cn/hutool/core/text/TextSimilarity.java b/hutool-core/src/main/java/cn/hutool/core/text/TextSimilarity.java new file mode 100644 index 000000000..c37dc89fa --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/text/TextSimilarity.java @@ -0,0 +1,132 @@ +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 { + + /** + * 计算相似度,两个都是空串相似度为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; + } + + int temp2 = longestCommonSubstring(newStrA, newStrB).length(); + return NumberUtil.div(temp2, temp); + } + + /** + * 计算相似度百分比 + * + * @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); + } + + // --------------------------------------------------------------------------------------------------- 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(false == isInvalidChar(c)) { + sb.append(c); + } + } + + return sb.toString(); + } + + /** + * 判断字符是否为非汉字,数字和字母, 因为对符号进行相似度比较没有实际意义,故符号不加入考虑范围。 + * + * @param charValue 字符 + * @return true表示为非汉字,数字和字母,false反之 + */ + private static boolean isInvalidChar(char charValue) { + return (charValue >= 0x4E00 && charValue <= 0XFFF) || // + (charValue >= 'a' && charValue <= 'z') || // + (charValue >= 'A' && charValue <= 'Z') || // + (charValue >= '0' && charValue <= '9'); + } + + /** + * 求公共子串,采用动态规划算法。 其不要求所求得的字符在所给的字符串中是连续的。 + * + * @param strA 字符串1 + * @param strB 字符串2 + * @return 公共子串 + */ + private static String longestCommonSubstring(String strA, String strB) { + char[] chars_strA = strA.toCharArray(); + char[] chars_strB = strB.toCharArray(); + int m = chars_strA.length; + int n = chars_strB.length; + + // 初始化矩阵数据,matrix[0][0]的值为0, 如果字符数组chars_strA和chars_strB的对应位相同,则matrix[i][j]的值为左上角的值加1, 否则,matrix[i][j]的值等于左上方最近两个位置的较大值, 矩阵中其余各点的值为0. + int[][] matrix = new int[m + 1][n + 1]; + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + if (chars_strA[i - 1] == chars_strB[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]); + } + } + } + + // 矩阵中,如果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] = chars_strA[m - 1]; + currentIndex--; + n--; + m--; + } + } + return new String(result); + } + // --------------------------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-core/src/main/java/cn/hutool/core/text/UnicodeUtil.java b/hutool-core/src/main/java/cn/hutool/core/text/UnicodeUtil.java new file mode 100644 index 000000000..893236996 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/text/UnicodeUtil.java @@ -0,0 +1,93 @@ +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(); + StrBuilder sb = StrBuilder.create(len); + int i = -1; + 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 = 0; + 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 { + pos = i;//非Unicode符,结束 + break; + } + } + + if(pos < len) { + sb.append(unicode,pos, len); + } + return sb.toString(); + } + + /** + * 字符串编码为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 StrBuilder unicode = StrBuilder.create(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/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvConfig.java b/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvConfig.java new file mode 100644 index 000000000..65aa48c07 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvConfig.java @@ -0,0 +1,38 @@ +package cn.hutool.core.text.csv; + +import java.io.Serializable; + +import cn.hutool.core.util.CharUtil; + +/** + * CSV基础配置项 + * + * @author looly + * @since 4.0.5 + */ +public class CsvConfig implements Serializable{ + private static final long serialVersionUID = -8069578249066158459L; + + /** 字段分隔符,默认逗号',' */ + protected char fieldSeparator = CharUtil.COMMA; + /** 文本分隔符,文本包装符,默认双引号'"' */ + protected char textDelimiter = CharUtil.DOUBLE_QUOTES; + + /** + * 设置字段分隔符,默认逗号',' + * + * @param fieldSeparator 字段分隔符,默认逗号',' + */ + public void setFieldSeparator(final char fieldSeparator) { + this.fieldSeparator = fieldSeparator; + } + + /** + * 设置 文本分隔符,文本包装符,默认双引号'"' + * + * @param textDelimiter 文本分隔符,文本包装符,默认双引号'"' + */ + public void setTextDelimiter(char textDelimiter) { + this.textDelimiter = textDelimiter; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvData.java b/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvData.java new file mode 100644 index 000000000..e6e23ed42 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvData.java @@ -0,0 +1,72 @@ +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 头信息 + * @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() { + 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(); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvParser.java b/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvParser.java new file mode 100644 index 000000000..50f838e7d --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvParser.java @@ -0,0 +1,266 @@ +package cn.hutool.core.text.csv; + +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; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.text.StrBuilder; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; + +/** + * CSV行解析器,参考:FastCSV + * + * @author Looly + */ +public final class CsvParser 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 char[] buf = new char[IoUtil.DEFAULT_LARGE_BUFFER_SIZE]; + /** 当前位置 */ + private int bufPos; + /** 读取一段后数据长度 */ + private int bufLen; + /** 拷贝开始的位置,一般为上一行的结束位置 */ + private int copyStart; + /** 前一个特殊分界字符 */ + private int preChar = -1; + /** 是否在引号包装内 */ + private boolean inQuotes; + /** 当前读取字段 */ + private final StrBuilder currentField = new StrBuilder(512); + + /** 标题行 */ + private CsvRow header; + /** 当前行号 */ + private long lineNo; + /** 第一行字段数,用于检查每行字段数是否一致 */ + 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()); + } + + /** + * 获取头部字段列表,如果containsHeader设置为false则抛出异常 + * + * @return 头部列表 + * @throws IllegalStateException 如果不解析头部或者没有调用nextRow()方法 + */ + public List getHeader() { + if (false == config.containsHeader) { + throw new IllegalStateException("No header available - header parsing is disabled"); + } + if (lineNo == 0) { + throw new IllegalStateException("No header available - call nextRow() first"); + } + return header.fields; + } + + /** + *读取下一行数据 + * + * @return CsvRow + * @throws IORuntimeException IO读取异常 + */ + public CsvRow nextRow() throws IORuntimeException { + long startingLineNo; + List currentFields; + int fieldCount; + while (false == finished) { + startingLineNo = ++lineNo; + currentFields = readLine(); + if(null == currentFields) { + break; + } + fieldCount = currentFields.size(); + // 末尾 + if (fieldCount == 0) { + break; + } + + // 跳过空行 + if (config.skipEmptyRows && fieldCount == 1 && currentFields.get(0).isEmpty()) { + continue; + } + + // 检查每行的字段数是否一致 + if (config.errorOnDifferentFieldCount) { + if (firstLineFieldCount == -1) { + 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 (config.containsHeader && null == header) { + initHeader(currentFields); + // 作为标题行后,此行跳过,下一行做为第一行 + continue; + } + + return new CsvRow(startingLineNo, 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++) { + final String field = currentFields.get(i); + if (StrUtil.isNotEmpty(field) && false ==localHeaderMap.containsKey(field)) { + localHeaderMap.put(field, i); + } + } + + header = new CsvRow(this.lineNo, Collections.unmodifiableMap(localHeaderMap), Collections.unmodifiableList(currentFields)); + } + + /** + * 读取一行数据 + * + * @return 一行数据 + * @throws IORuntimeException IO异常 + */ + private List readLine() throws IORuntimeException { + final List currentFields = new ArrayList<>(maxFieldCount > 0 ? maxFieldCount : DEFAULT_ROW_CAPACITY); + + final StrBuilder localCurrentField = currentField; + final char[] localBuf = this.buf; + int localBufPos = bufPos;//当前位置 + int localPreChar = preChar;//前一个特殊分界字符 + int localCopyStart = copyStart;//拷贝起始位置 + int copyLen = 0; //拷贝长度 + + while (true) { + if (bufLen == localBufPos) { + // 此Buffer读取结束,开始读取下一段 + + if (copyLen > 0) { + localCurrentField.append(localBuf, localCopyStart, copyLen); + } + try { + bufLen = reader.read(localBuf); + } catch (IOException e) { + throw new IORuntimeException(e); + } + + if (bufLen < 0) { + // CSV读取结束 + finished = true; + + if (localPreChar == config.fieldSeparator || localCurrentField.hasContent()) { + //剩余部分作为一个字段 + currentFields.add(localCurrentField.toStringAndReset()); + } + break; + } + + //重置 + localCopyStart = localBufPos = copyLen = 0; + } + + final char c = localBuf[localBufPos++]; + + if (inQuotes) { + //引号内,做为内容,直到引号结束 + if (c == config.textDelimiter) { + // End of quoted text + inQuotes = false; + } else { + if ((c == CharUtil.CR || c == CharUtil.LF) && localPreChar != CharUtil.CR) { + lineNo++; + } + } + copyLen++; + } else { + if (c == config.fieldSeparator) { + //一个字段结束 + if (copyLen > 0) { + localCurrentField.append(localBuf, localCopyStart, copyLen); + copyLen = 0; + } + currentFields.add(StrUtil.unWrap(localCurrentField.toStringAndReset(), config.textDelimiter)); + localCopyStart = localBufPos; + } else if (c == config.textDelimiter) { + // 引号开始 + inQuotes = true; + copyLen++; + } else if (c == CharUtil.CR) { + if (copyLen > 0) { + localCurrentField.append(localBuf, localCopyStart, copyLen); + } + currentFields.add(StrUtil.unWrap(localCurrentField.toStringAndReset(), config.textDelimiter)); + localPreChar = c; + localCopyStart = localBufPos; + break; + } else if (c == CharUtil.LF) { + if (localPreChar != CharUtil.CR) { + if (copyLen > 0) { + localCurrentField.append(localBuf, localCopyStart, copyLen); + } + currentFields.add(StrUtil.unWrap(localCurrentField.toStringAndReset(), config.textDelimiter)); + localPreChar = c; + localCopyStart = localBufPos; + break; + } + localCopyStart = localBufPos; + } else { + copyLen++; + } + } + + localPreChar = c; + } + + // restore fields + bufPos = localBufPos; + preChar = localPreChar; + copyStart = localCopyStart; + + return currentFields; + } + + @Override + public void close() throws IOException { + reader.close(); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvReadConfig.java b/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvReadConfig.java new file mode 100644 index 000000000..855470ddb --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvReadConfig.java @@ -0,0 +1,56 @@ +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; + + /** 是否首行做为标题行,默认false */ + protected boolean containsHeader; + /** 是否跳过空白行,默认true */ + protected boolean skipEmptyRows = true; + /** 每行字段个数不同时是否抛出异常,默认false */ + protected boolean errorOnDifferentFieldCount; + + /** + * 默认配置 + * + * @return 默认配置 + */ + public static CsvReadConfig defaultConfig() { + return new CsvReadConfig(); + } + + /** + * 设置是否首行做为标题行,默认false + * + * @param containsHeader 是否首行做为标题行,默认false + */ + public void setContainsHeader(boolean containsHeader) { + this.containsHeader = containsHeader; + } + + /** + * 设置是否跳过空白行,默认true + * + * @param skipEmptyRows 是否跳过空白行,默认true + */ + public void setSkipEmptyRows(boolean skipEmptyRows) { + this.skipEmptyRows = skipEmptyRows; + } + + /** + * 设置每行字段个数不同时是否抛出异常,默认false + * + * @param errorOnDifferentFieldCount 每行字段个数不同时是否抛出异常,默认false + */ + public void setErrorOnDifferentFieldCount(boolean errorOnDifferentFieldCount) { + this.errorOnDifferentFieldCount = errorOnDifferentFieldCount; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvReader.java b/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvReader.java new file mode 100644 index 000000000..ef123ba5e --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvReader.java @@ -0,0 +1,172 @@ +package cn.hutool.core.text.csv; + +import java.io.File; +import java.io.IOException; +import java.io.Reader; +import java.io.Serializable; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.ObjectUtil; + +/** + * CSV文件读取器,参考:FastCSV + * + * @author Looly + * @since 4.0.1 + */ +public final class CsvReader implements Serializable{ + private static final long serialVersionUID = 1L; + + CsvReadConfig config; + + /** + * 构造,使用默认配置项 + */ + public CsvReader() { + this(null); + } + + /** + * 构造 + * + * @param config 配置项 + */ + public CsvReader(CsvReadConfig config) { + this.config = ObjectUtil.defaultIfNull(config, CsvReadConfig.defaultConfig()); + } + + /** + * 设置字段分隔符,默认逗号',' + * + * @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.setErrorOnDifferentFieldCount(errorOnDifferentFieldCount); + } + + /** + * 读取CSV文件,默认UTF-8编码 + * + * @param file CSV文件 + * @return {@link CsvData},包含数据列表和行信息 + * @throws IORuntimeException IO异常 + */ + public CsvData read(File file) throws IORuntimeException { + return read(file, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 读取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, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 读取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"); + try (Reader reader = FileUtil.getReader(path, charset)) { + return read(reader); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 从Reader中读取CSV数据 + * + * @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<>(); + CsvRow csvRow; + while ((csvRow = csvParser.nextRow()) != null) { + rows.add(csvRow); + } + + final List header = config.containsHeader ? csvParser.getHeader() : null; + return new CsvData(header, rows); + } + + /** + * 构建 {@link CsvParser} + * + * @param reader Reader + * @return CsvParser + * @throws IORuntimeException IO异常 + */ + private CsvParser parse(Reader reader) throws IORuntimeException { + return new CsvParser(reader, config); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvRow.java b/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvRow.java new file mode 100644 index 000000000..0858426f0 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvRow.java @@ -0,0 +1,253 @@ +package cn.hutool.core.text.csv; + +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(final long originalLineNumber, final Map headerMap, final List fields) { + + 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(final String name) { + if (headerMap == null) { + throw new IllegalStateException("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 an unmodifiable map of header names and field values of this row + * @throws IllegalStateException CSV文件无标题行抛出此异常 + */ + public Map getFieldMap() { + if (headerMap == null) { + throw new IllegalStateException("No header available"); + } + + final Map fieldMap = new LinkedHashMap<>(headerMap.size()); + 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; + } + + /** + * 获取字段格式 + * + * @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) { + 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/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvUtil.java b/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvUtil.java new file mode 100644 index 000000000..5ac7f84e3 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvUtil.java @@ -0,0 +1,110 @@ +package cn.hutool.core.text.csv; + +import java.io.File; +import java.io.Writer; +import java.nio.charset.Charset; + +/** + * CSV工具 + * + * @author looly + * @since 4.0.5 + */ +public class CsvUtil { + + //----------------------------------------------------------------------------------------------------------- Reader + /** + * 获取CSV读取器 + * + * @param config 配置 + * @return {@link CsvReader} + */ + public static CsvReader getReader(CsvReadConfig config) { + return new CsvReader(config); + } + + /** + * 获取CSV读取器 + * + * @return {@link CsvReader} + */ + public static CsvReader getReader() { + return new CsvReader(); + } + + //----------------------------------------------------------------------------------------------------------- Writer + /** + * 获取CSV生成器(写出器),使用默认配置,覆盖已有文件(如果存在) + * + * @param filePath File CSV文件路径 + * @param charset 编码 + */ + public static CsvWriter getWriter(String filePath, Charset charset) { + return new CsvWriter(filePath, charset); + } + + /** + * 获取CSV生成器(写出器),使用默认配置,覆盖已有文件(如果存在) + * + * @param file File CSV文件 + * @param charset 编码 + */ + public static CsvWriter getWriter(File file, Charset charset) { + return new CsvWriter(file, charset); + } + + /** + * 获取CSV生成器(写出器),使用默认配置 + * + * @param filePath File CSV文件路径 + * @param charset 编码 + * @param isAppend 是否追加 + */ + 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 是否追加 + */ + 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则使用默认配置 + */ + 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/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvWriteConfig.java b/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvWriteConfig.java new file mode 100644 index 000000000..719fd8bc9 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvWriteConfig.java @@ -0,0 +1,47 @@ +package cn.hutool.core.text.csv; + +import java.io.Serializable; + +import cn.hutool.core.util.CharUtil; + +/** + * 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,按需添加 + */ + public void setAlwaysDelimitText(boolean alwaysDelimitText) { + this.alwaysDelimitText = alwaysDelimitText; + } + + /** + * 设置换行符 + * + * @param lineDelimiter 换行符 + */ + public void setLineDelimiter(char[] lineDelimiter) { + this.lineDelimiter = lineDelimiter; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvWriter.java b/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvWriter.java new file mode 100644 index 000000000..7db1433cf --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/text/csv/CsvWriter.java @@ -0,0 +1,322 @@ +package cn.hutool.core.text.csv; + +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.Collection; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.ObjectUtil; + +/** + * 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; + + // --------------------------------------------------------------------------------------------------- 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 { + if (ArrayUtil.isNotEmpty(lines)) { + for (final String[] values : lines) { + appendLine(values); + } + flush(); + } + return this; + } + + /** + * 将多行写出到Writer + * + * @param lines 多行数据 + * @return this + * @throws IORuntimeException IO异常 + */ + public CsvWriter write(Collection lines) throws IORuntimeException { + if (CollUtil.isNotEmpty(lines)) { + for (final String[] values : lines) { + appendLine(values); + } + flush(); + } + return this; + } + + /** + * 追加新行(换行) + * + * @throws IORuntimeException IO异常 + */ + public void writeLine() throws IORuntimeException { + try { + writer.write(config.lineDelimiter); + } catch (IOException e) { + throw new IORuntimeException(e); + } + newline = true; + } + + @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(final String... fields) throws IORuntimeException { + try { + doAppendLine(fields); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 追加一行,末尾会自动换行,但是追加前不会换行 + * + * @param fields 字段列表 ({@code null} 值会被做为空值追加) + * @throws IOException IO异常 + */ + private void doAppendLine(final String... fields) throws IOException { + if (null != fields) { + for (int i = 0; i < fields.length; i++) { + appendField(fields[i]); + } + writer.write(config.lineDelimiter); + 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 (false == 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/hutool-core/src/main/java/cn/hutool/core/text/csv/package-info.java b/hutool-core/src/main/java/cn/hutool/core/text/csv/package-info.java new file mode 100644 index 000000000..c787e5773 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/text/csv/package-info.java @@ -0,0 +1,7 @@ +/** + * 提供CSV文件读写的封装,入口为CsvUtil + * + * @author looly + * + */ +package cn.hutool.core.text.csv; \ No newline at end of file diff --git a/hutool-core/src/main/java/cn/hutool/core/text/escape/Html4Escape.java b/hutool-core/src/main/java/cn/hutool/core/text/escape/Html4Escape.java new file mode 100644 index 000000000..e44b818b0 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/text/escape/Html4Escape.java @@ -0,0 +1,322 @@ +package cn.hutool.core.text.escape; + +import cn.hutool.core.text.replacer.LookupReplacer; +import cn.hutool.core.text.replacer.ReplacerChain; + +/** + * HTML4的ESCAPE + * @author looly + * + */ +public class Html4Escape extends ReplacerChain { + private static final long serialVersionUID = 1L; + + protected static final String[][] BASIC_ESCAPE = { // + { "\"", """ }, // " - double-quote + { "&", "&" }, // & - ampersand + { "<", "<" }, // < - less-than + { ">", ">" }, // > - greater-than + }; + + 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() { + addChain(new LookupReplacer(BASIC_ESCAPE)); + addChain(new LookupReplacer(ISO8859_1_ESCAPE)); + addChain(new LookupReplacer(HTML40_EXTENDED_ESCAPE)); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/text/escape/Html4Unescape.java b/hutool-core/src/main/java/cn/hutool/core/text/escape/Html4Unescape.java new file mode 100644 index 000000000..b6d4de9ef --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/text/escape/Html4Unescape.java @@ -0,0 +1,25 @@ +package cn.hutool.core.text.escape; + +import cn.hutool.core.text.replacer.LookupReplacer; +import cn.hutool.core.text.replacer.ReplacerChain; + +/** + * HTML4的UNESCAPE + * + * @author looly + * + */ +public class Html4Unescape extends ReplacerChain { + private static final long serialVersionUID = 1L; + + protected static final String[][] BASIC_UNESCAPE = InternalEscapeUtil.invert(Html4Escape.BASIC_ESCAPE); + 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() { + addChain(new LookupReplacer(BASIC_UNESCAPE)); + addChain(new LookupReplacer(ISO8859_1_UNESCAPE)); + addChain(new LookupReplacer(HTML40_EXTENDED_UNESCAPE)); + addChain(new NumericEntityUnescaper()); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/text/escape/InternalEscapeUtil.java b/hutool-core/src/main/java/cn/hutool/core/text/escape/InternalEscapeUtil.java new file mode 100644 index 000000000..4de32aa3c --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/text/escape/NumericEntityUnescaper.java b/hutool-core/src/main/java/cn/hutool/core/text/escape/NumericEntityUnescaper.java new file mode 100644 index 000000000..fa5e82780 --- /dev/null +++ b/hutool-core/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) + (isSemiNext ? 1 : 0); + } + } + return 0; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/text/escape/package-info.java b/hutool-core/src/main/java/cn/hutool/core/text/escape/package-info.java new file mode 100644 index 000000000..522cb94f6 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/text/package-info.java b/hutool-core/src/main/java/cn/hutool/core/text/package-info.java new file mode 100644 index 000000000..bde5edb79 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/text/replacer/LookupReplacer.java b/hutool-core/src/main/java/cn/hutool/core/text/replacer/LookupReplacer.java new file mode 100644 index 000000000..661726706 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/text/replacer/LookupReplacer.java @@ -0,0 +1,74 @@ +package cn.hutool.core.text.replacer; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import cn.hutool.core.text.StrBuilder; + +/** + * 查找替换器,通过查找指定关键字,替换对应的值 + * + * @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/hutool-core/src/main/java/cn/hutool/core/text/replacer/ReplacerChain.java b/hutool-core/src/main/java/cn/hutool/core/text/replacer/ReplacerChain.java new file mode 100644 index 000000000..d80eb2a9c --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/text/replacer/ReplacerChain.java @@ -0,0 +1,55 @@ +package cn.hutool.core.text.replacer; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import cn.hutool.core.lang.Chain; +import cn.hutool.core.text.StrBuilder; + +/** + * 字符串替换链,用于组合多个字符串替换逻辑 + * + * @author looly + * @since 4.1.5 + */ +public class ReplacerChain extends StrReplacer implements Chain { + private static final long serialVersionUID = 1L; + + private List replacers = new LinkedList<>(); + + /** + * 构造 + * + * @param strReplacers 字符串替换器 + */ + public ReplacerChain(StrReplacer... strReplacers) { + for (StrReplacer strReplacer : strReplacers) { + addChain(strReplacer); + } + } + + @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/hutool-core/src/main/java/cn/hutool/core/text/replacer/StrReplacer.java b/hutool-core/src/main/java/cn/hutool/core/text/replacer/StrReplacer.java new file mode 100644 index 000000000..2683a87d1 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/text/replacer/StrReplacer.java @@ -0,0 +1,44 @@ +package cn.hutool.core.text.replacer; + +import java.io.Serializable; + +import cn.hutool.core.lang.Replacer; +import cn.hutool.core.text.StrBuilder; + +/** + * 抽象字符串替换类
+ * 通过实现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 strBuillder = StrBuilder.create(len); + int pos = 0;//当前位置 + int consumed;//处理过的字符数 + while(pos < len) { + consumed = replace(t, pos, strBuillder); + if(0 == consumed) { + //0表示未处理或替换任何字符,原样输出本字符并从下一个字符继续 + strBuillder.append(t.charAt(pos)); + pos++; + } + pos += consumed; + } + return strBuillder; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/text/replacer/package-info.java b/hutool-core/src/main/java/cn/hutool/core/text/replacer/package-info.java new file mode 100644 index 000000000..af302856b --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/thread/ConcurrencyTester.java b/hutool-core/src/main/java/cn/hutool/core/thread/ConcurrencyTester.java new file mode 100644 index 000000000..ee7d770b5 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/thread/ConcurrencyTester.java @@ -0,0 +1,53 @@ +package cn.hutool.core.thread; + +import cn.hutool.core.date.TimeInterval; + +/** + * 高并发测试工具类 + * + *
+ * ps:
+ * //模拟1000个线程并发
+ * ConcurrencyTester ct = new ConcurrencyTester(1000);
+ * ct.test(() -> {
+ *      // 需要并发测试的业务代码
+ * });
+ * 
+ * + * @author kwer + */ +public class ConcurrencyTester { + private SyncFinisher sf; + private TimeInterval timeInterval; + private long interval; + + public ConcurrencyTester(int threadSize) { + this.sf = new SyncFinisher(threadSize); + this.timeInterval = new TimeInterval(); + } + + /** + * 执行测试 + * + * @param runnable 要测试的内容 + */ + public ConcurrencyTester test(Runnable runnable) { + timeInterval.start(); + this.sf// + .addRepeatWorker(runnable)// + .setBeginAtSameTime(true)// 同时开始 + .start(); + + this.interval = timeInterval.interval(); + return this; + } + + /** + * 获取执行时间 + * + * @return 执行时间,单位毫秒 + */ + public long getInterval() { + return this.interval; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/thread/ExecutorBuilder.java b/hutool-core/src/main/java/cn/hutool/core/thread/ExecutorBuilder.java new file mode 100644 index 000000000..1ef7865bd --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/thread/ExecutorBuilder.java @@ -0,0 +1,211 @@ +package cn.hutool.core.thread; + +import java.util.concurrent.BlockingQueue; +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; + +import cn.hutool.core.builder.Builder; +import cn.hutool.core.util.ObjectUtil; + +/** + * {@link ThreadPoolExecutor} 建造者 + * + * @author looly + * @since 4.1.9 + */ +public class ExecutorBuilder implements Builder { + private static final long serialVersionUID = 1L; + + /** 初始池大小 */ + 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. SynchronousQueue    它将任务直接提交给线程而不保持它们。当运行线程小于maxPoolSize时会创建新线程,否则触发异常策略
+	 * 2. LinkedBlockingQueue 无界队列,当运行线程大于corePoolSize时始终放入此队列,此时maximumPoolSize无效
+	 * 3. ArrayBlockingQueue  有界队列,相对无界队列有利于控制队列大小,队列满时,运行线程小于maxPoolSize时会创建新线程,否则触发异常策略
+	 * 
+ * + * @param workQueue 队列 + * @return this + */ + public ExecutorBuilder setWorkQueue(BlockingQueue workQueue) { + this.workQueue = workQueue; + return this; + } + + /** + * 使用{@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 {@link ExecutorBuilder} + */ + public static ExecutorBuilder create() { + return new ExecutorBuilder(); + } + + /** + * 构建ThreadPoolExecutor + */ + @Override + public ThreadPoolExecutor build() { + return build(this); + } + + /** + * 构建ThreadPoolExecutor + * + * @param builder {@link ExecutorBuilder} + * @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(); + } + final ThreadFactory threadFactory = (null != builder.threadFactory) ? builder.threadFactory : Executors.defaultThreadFactory(); + RejectedExecutionHandler handler = ObjectUtil.defaultIfNull(builder.handler, new ThreadPoolExecutor.AbortPolicy()); + + 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/hutool-core/src/main/java/cn/hutool/core/thread/GlobalThreadPool.java b/hutool-core/src/main/java/cn/hutool/core/thread/GlobalThreadPool.java new file mode 100644 index 000000000..fb2d096c9 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/thread/NamedThreadFactory.java b/hutool-core/src/main/java/cn/hutool/core/thread/NamedThreadFactory.java new file mode 100644 index 000000000..ccc12a495 --- /dev/null +++ b/hutool-core/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 isDeamon; + /** 无法捕获的异常统一处理 */ + private final UncaughtExceptionHandler handler; + + /** + * 构造 + * + * @param prefix 线程名前缀 + * @param isDeamon 是否守护线程 + */ + public NamedThreadFactory(String prefix, boolean isDeamon) { + this(prefix, null, isDeamon); + } + + /** + * 构造 + * + * @param prefix 线程名前缀 + * @param threadGroup 线程组,可以为null + * @param isDeamon 是否守护线程 + */ + public NamedThreadFactory(String prefix, ThreadGroup threadGroup, boolean isDeamon) { + this(prefix, threadGroup, isDeamon, null); + } + + /** + * 构造 + * + * @param prefix 线程名前缀 + * @param threadGroup 线程组,可以为null + * @param isDeamon 是否守护线程 + * @param handler 未捕获异常处理 + */ + public NamedThreadFactory(String prefix, ThreadGroup threadGroup, boolean isDeamon, UncaughtExceptionHandler handler) { + this.prefix = StrUtil.isBlank(prefix) ? "Hutool" : prefix; + if (null == threadGroup) { + threadGroup = ThreadUtil.currentThreadGroup(); + } + this.group = threadGroup; + this.isDeamon = isDeamon; + this.handler = handler; + } + + @Override + public Thread newThread(Runnable r) { + final Thread t = new Thread(this.group, r, StrUtil.format("{}{}", prefix, threadNumber.getAndIncrement())); + + //守护线程 + if (false == t.isDaemon()) { + if (isDeamon) { + // 原线程为非守护则设置为守护 + t.setDaemon(true); + } + } else if (false == isDeamon) { + // 原线程为守护则还原为非守护 + 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/hutool-core/src/main/java/cn/hutool/core/thread/RejectPolicy.java b/hutool-core/src/main/java/cn/hutool/core/thread/RejectPolicy.java new file mode 100644 index 000000000..3ecbe3123 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/thread/RejectPolicy.java @@ -0,0 +1,40 @@ +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()); + + private RejectedExecutionHandler value; + + private RejectPolicy(RejectedExecutionHandler handler) { + this.value = handler; + } + + /** + * 获取RejectedExecutionHandler枚举值 + * + * @return RejectedExecutionHandler + */ + public RejectedExecutionHandler getValue() { + return this.value; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/thread/SemaphoreRunnable.java b/hutool-core/src/main/java/cn/hutool/core/thread/SemaphoreRunnable.java new file mode 100644 index 000000000..9cdcfc304 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/thread/SemaphoreRunnable.java @@ -0,0 +1,47 @@ +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 Runnable runnable; + /** 信号量 */ + private Semaphore semaphore; + + /** + * 构造 + * + * @param runnable 实际执行的线程逻辑 + * @param semaphore 信号量,多个线程必须共享同一信号量 + */ + public SemaphoreRunnable(Runnable runnable, Semaphore semaphore) { + this.runnable = runnable; + this.semaphore = semaphore; + } + + @Override + public void run() { + if (null != this.semaphore) { + try { + semaphore.acquire(); + this.runnable.run(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + semaphore.release(); + } + } + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/thread/SyncFinisher.java b/hutool-core/src/main/java/cn/hutool/core/thread/SyncFinisher.java new file mode 100644 index 000000000..63535476c --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/thread/SyncFinisher.java @@ -0,0 +1,194 @@ +package cn.hutool.core.thread; + +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; + +import cn.hutool.core.exceptions.NotInitedException; +import cn.hutool.core.exceptions.UtilException; + +/** + * 线程同步结束器
+ * 在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。 + * + *
+ * ps:
+ * //模拟1000个线程并发
+ * SyncFinisher sf = new SyncFinisher(1000);
+ * concurrencyTestUtil.run(() -> {
+ *      // 需要并发测试的业务代码
+ * });
+ * 
+ * + * + * @author Looly + * @since 4.1.15 + */ +public class SyncFinisher { + + private Set workers; + private int threadSize; + private ExecutorService executorService; + + private boolean isBeginAtSameTime; + /** 启动同步器,用于保证所有worker线程同时开始 */ + private CountDownLatch beginLatch; + /** 结束同步器,用于等待所有worker线程同时结束 */ + private CountDownLatch endLatch; + + /** + * 构造 + * + * @param threadSize 线程数 + */ + public SyncFinisher(int threadSize) { + this.beginLatch = new CountDownLatch(1); + this.threadSize = threadSize; + this.executorService = ThreadUtil.newExecutor(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; + } + + /** + * 开始工作 + */ + public void start() { + start(true); + } + + /** + * 开始工作 + * + * @param sync 是否阻塞等待 + * @since 4.5.8 + */ + public void start(boolean sync) { + endLatch = new CountDownLatch(workers.size()); + for (Worker worker : workers) { + executorService.submit(worker); + } + // 保证所有worker同时开始 + this.beginLatch.countDown(); + + if (sync) { + try { + this.endLatch.await(); + } catch (InterruptedException e) { + throw new UtilException(e); + } + } + } + + /** + * 等待所有Worker工作结束,否则阻塞 + * + * @throws InterruptedException 用户中断 + * @deprecated 使用start方法指定是否阻塞等待 + */ + @Deprecated + public void await() throws InterruptedException { + if (endLatch == null) { + throw new NotInitedException("Please call start() method first!"); + } + + endLatch.await(); + } + + /** + * 清空工作线程对象 + */ + public void clearWorker() { + workers.clear(); + } + + /** + * 剩余任务数 + * + * @return 剩余任务数 + */ + public long count() { + return endLatch.getCount(); + } + + /** + * 工作者,为一个线程 + * + * @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/hutool-core/src/main/java/cn/hutool/core/thread/ThreadFactoryBuilder.java b/hutool-core/src/main/java/cn/hutool/core/thread/ThreadFactoryBuilder.java new file mode 100644 index 000000000..7fb73f78c --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/thread/ThreadFactoryBuilder.java @@ -0,0 +1,148 @@ +package cn.hutool.core.thread; + +import java.lang.Thread.UncaughtExceptionHandler; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicLong; + +import cn.hutool.core.builder.Builder; +import cn.hutool.core.util.StrUtil; + +/** + * 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; + + /** + * 创建{@link ThreadFactoryBuilder} + * + * @return {@link 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} + */ + public void setUncaughtExceptionHandler(UncaughtExceptionHandler uncaughtExceptionHandler) { + this.uncaughtExceptionHandler = uncaughtExceptionHandler; + } + + /** + * 构建{@link ThreadFactory} + * + * @return {@link ThreadFactory} + */ + @Override + public ThreadFactory build() { + return build(this); + } + + /** + * 构建 + * + * @param builder {@link 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 new ThreadFactory() { + @Override + public Thread newThread(Runnable 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/hutool-core/src/main/java/cn/hutool/core/thread/ThreadUtil.java b/hutool-core/src/main/java/cn/hutool/core/thread/ThreadUtil.java new file mode 100644 index 000000000..8756d5e92 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/thread/ThreadUtil.java @@ -0,0 +1,457 @@ +package cn.hutool.core.thread; + +import java.lang.Thread.UncaughtExceptionHandler; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletionService; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorCompletionService; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * 线程池工具 + * + * @author luxiaolei + */ +public class ThreadUtil { + + /** + * 新建一个线程池 + * + * @param threadSize 同时执行的线程数大小 + * @return ExecutorService + */ + public static ExecutorService newExecutor(int threadSize) { + ExecutorBuilder builder = ExecutorBuilder.create(); + if (threadSize > 0) { + builder.setCorePoolSize(threadSize); + } + return builder.build(); + } + + /** + * 获得一个新的线程池 + * + * @return ExecutorService + */ + public static ExecutorService newExecutor() { + return ExecutorBuilder.create().useSynchronousQueue().build(); + } + + /** + * 获得一个新的线程池,只有单个线程 + * + * @return ExecutorService + */ + public static ExecutorService newSingleExecutor() { + return Executors.newSingleThreadExecutor(); + } + + /** + * 获得一个新的线程池
+ * 如果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(); + } + + /** + * 获得一个新的线程池
+ * 传入阻塞系数,线程池的大小计算公式为:CPU可用核心数 / (1 - 阻塞因子)
+ * Blocking Coefficient(阻塞系数) = 阻塞时间/(阻塞时间+使用CPU的时间)
+ * 计算密集型任务的阻塞系数为0,而IO密集型任务的阻塞系数则接近于1。 + * + * see: http://blog.csdn.net/partner4java/article/details/9417663 + * + * @param blockingCoefficient 阻塞系数,阻塞因子介于0~1之间的数,阻塞因子越大,线程池中的线程数越多。 + * @return {@link ThreadPoolExecutor} + * @since 3.0.6 + */ + public static ThreadPoolExecutor newExecutorByBlockingCoefficient(float blockingCoefficient) { + if (blockingCoefficient >= 1 || blockingCoefficient < 0) { + throw new IllegalArgumentException("[blockingCoefficient] must between 0 and 1, or equals 0."); + } + + // 最佳的线程数 = CPU可用核心数 / (1 - 阻塞系数) + int poolSize = (int) (Runtime.getRuntime().availableProcessors() / (1 - blockingCoefficient)); + return ExecutorBuilder.create().setCorePoolSize(poolSize).setMaxPoolSize(poolSize).setKeepAliveTime(0L).build(); + } + + /** + * 直接在公共线程池中执行线程 + * + * @param runnable 可运行对象 + */ + public static void execute(Runnable runnable) { + GlobalThreadPool.execute(runnable); + } + + /** + * 执行异步方法 + * + * @param runnable 需要执行的方法体 + * @param isDeamon 是否守护线程。守护线程会在主线程结束后自动结束 + * @return 执行的方法体 + */ + public static Runnable excAsync(final Runnable runnable, boolean isDeamon) { + Thread thread = new Thread() { + @Override + public void run() { + runnable.run(); + } + }; + thread.setDaemon(isDeamon); + 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 isDeamon 是否守护线程 + * @return {@link Thread} + * @since 4.1.2 + */ + public static Thread newThread(Runnable runnable, String name, boolean isDeamon) { + final Thread t = new Thread(null, runnable, name); + t.setDaemon(isDeamon); + 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; + } + + try { + Thread.sleep(millis.longValue()); + } catch (InterruptedException e) { + return false; + } + return true; + } + + /** + * 考虑{@link Thread#sleep(long)}方法有可能时间不足给定毫秒数,此方法保证sleep时间不小于给定的毫秒数 + * + * @see ThreadUtil#sleep(Number) + * @param millis 给定的sleep时间 + * @return 被中断返回false,否则true + */ + public static boolean safeSleep(Number millis) { + long millisLong = millis.longValue(); + long done = 0; + while (done < millisLong) { + long before = System.currentTimeMillis(); + if (false == sleep(millisLong - done)) { + return false; + } + long after = System.currentTimeMillis(); + done += (after - before); + } + 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<>(); + } + } + + /** + * 创建ThreadFactoryBuilder + * + * @return ThreadFactoryBuilder + * @see ThreadFactoryBuilder#build() + * @since 4.1.13 + */ + public static ThreadFactoryBuilder createThreadFactoryBuilder() { + return ThreadFactoryBuilder.create(); + } + + /** + * 结束线程,调用此方法后,线程将抛出 {@link InterruptedException}异常 + * + * @param thread 线程 + * @param isJoin 是否等待结束 + */ + public static void interupt(Thread thread, boolean isJoin) { + if (null != thread && false == thread.isInterrupted()) { + thread.interrupt(); + if (isJoin) { + waitForDie(thread); + } + } + } + + /** + * 等待线程结束. 调用 {@link Thread#join()} 并忽略 {@link InterruptedException} + * + * @param thread 线程 + */ + public static void waitForDie(Thread thread) { + 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 isDeamon 是否守护线程 + * @since 4.0.0 + */ + public static ThreadFactory newNamedThreadFactory(String prefix, boolean isDeamon) { + return new NamedThreadFactory(prefix, isDeamon); + } + + /** + * 创建线程工厂 + * + * @param prefix 线程名前缀 + * @param threadGroup 线程组,可以为null + * @param isDeamon 是否守护线程 + * @since 4.0.0 + */ + public static ThreadFactory newNamedThreadFactory(String prefix, ThreadGroup threadGroup, boolean isDeamon) { + return new NamedThreadFactory(prefix, threadGroup, isDeamon); + } + + /** + * 创建线程工厂 + * + * @param prefix 线程名前缀 + * @param threadGroup 线程组,可以为null + * @param isDeamon 是否守护线程 + * @param handler 未捕获异常处理 + * @since 4.0.0 + */ + public static ThreadFactory newNamedThreadFactory(String prefix, ThreadGroup threadGroup, boolean isDeamon, UncaughtExceptionHandler handler) { + return new NamedThreadFactory(prefix, threadGroup, isDeamon, handler); + } + + /** + * 阻塞当前线程,保证在main方法中执行不被退出 + * + * @param obj 对象所在线程 + * @since 4.5.6 + */ + 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 + */ + public static ConcurrencyTester concurrencyTest(int threadSize, Runnable runnable) { + return (new ConcurrencyTester(threadSize)).test(runnable); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/thread/lock/NoLock.java b/hutool-core/src/main/java/cn/hutool/core/thread/lock/NoLock.java new file mode 100644 index 000000000..12032abb0 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/thread/lock/NoLock.java @@ -0,0 +1,42 @@ +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{ + + @Override + public void lock() { + } + + @Override + public void lockInterruptibly() throws InterruptedException { + } + + @Override + public boolean tryLock() { + return true; + } + + @Override + public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { + return true; + } + + @Override + public void unlock() { + } + + @Override + public Condition newCondition() { + return null; + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/thread/lock/package-info.java b/hutool-core/src/main/java/cn/hutool/core/thread/lock/package-info.java new file mode 100644 index 000000000..99f1fd002 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/thread/package-info.java b/hutool-core/src/main/java/cn/hutool/core/thread/package-info.java new file mode 100644 index 000000000..aefac6bd0 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/thread/threadlocal/NamedInheritableThreadLocal.java b/hutool-core/src/main/java/cn/hutool/core/thread/threadlocal/NamedInheritableThreadLocal.java new file mode 100644 index 000000000..a7eba1e87 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/thread/threadlocal/NamedThreadLocal.java b/hutool-core/src/main/java/cn/hutool/core/thread/threadlocal/NamedThreadLocal.java new file mode 100644 index 000000000..6eca0d26a --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/thread/threadlocal/package-info.java b/hutool-core/src/main/java/cn/hutool/core/thread/threadlocal/package-info.java new file mode 100644 index 000000000..5c73da0a8 --- /dev/null +++ b/hutool-core/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/hutool-core/src/main/java/cn/hutool/core/util/ArrayUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/ArrayUtil.java new file mode 100644 index 000000000..dff5eafaf --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/util/ArrayUtil.java @@ -0,0 +1,3830 @@ +package cn.hutool.core.util; + +import java.lang.reflect.Array; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.collection.IterUtil; +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.lang.Editor; +import cn.hutool.core.lang.Filter; + +/** + * 数组工具类 + * + * @author Looly + * + */ +public class ArrayUtil { + + /** 数组中元素未找到的下标,值为-1 */ + public static final int INDEX_NOT_FOUND = -1; + + // ---------------------------------------------------------------------- isEmpty + /** + * 数组是否为空 + * + * @param 数组元素类型 + * @param array 数组 + * @return 是否为空 + */ + @SuppressWarnings("unchecked") + public static boolean isEmpty(final T... array) { + return array == null || array.length == 0; + } + + /** + * 数组是否为空
+ * 此方法会匹配单一对象,如果此对象为{@code null}则返回true
+ * 如果此对象为非数组,理解为此对象为数组的第一个元素,则返回false
+ * 如果此对象为数组对象,数组长度大于0情况下返回false,否则返回true + * + * @param array 数组 + * @return 是否为空 + */ + public static boolean isEmpty(Object array) { + if (null == array) { + return true; + } else if (isArray(array)) { + return 0 == Array.getLength(array); + } + throw new UtilException("Object to provide is not a Array !"); + } + + /** + * 数组是否为空 + * + * @param array 数组 + * @return 是否为空 + */ + public static boolean isEmpty(final long... array) { + return array == null || array.length == 0; + } + + /** + * 数组是否为空 + * + * @param array 数组 + * @return 是否为空 + */ + public static boolean isEmpty(final int... array) { + return array == null || array.length == 0; + } + + /** + * 数组是否为空 + * + * @param array 数组 + * @return 是否为空 + */ + public static boolean isEmpty(final short... array) { + return array == null || array.length == 0; + } + + /** + * 数组是否为空 + * + * @param array 数组 + * @return 是否为空 + */ + public static boolean isEmpty(final char... array) { + return array == null || array.length == 0; + } + + /** + * 数组是否为空 + * + * @param array 数组 + * @return 是否为空 + */ + public static boolean isEmpty(final byte... array) { + return array == null || array.length == 0; + } + + /** + * 数组是否为空 + * + * @param array 数组 + * @return 是否为空 + */ + public static boolean isEmpty(final double... array) { + return array == null || array.length == 0; + } + + /** + * 数组是否为空 + * + * @param array 数组 + * @return 是否为空 + */ + public static boolean isEmpty(final float... array) { + return array == null || array.length == 0; + } + + /** + * 数组是否为空 + * + * @param array 数组 + * @return 是否为空 + */ + public static boolean isEmpty(final boolean... array) { + return array == null || array.length == 0; + } + + // ---------------------------------------------------------------------- isNotEmpty + /** + * 数组是否为非空 + * + * @param 数组元素类型 + * @param array 数组 + * @return 是否为非空 + */ + @SuppressWarnings("unchecked") + public static boolean isNotEmpty(final T... array) { + return (array != null && array.length != 0); + } + + /** + * 数组是否为非空
+ * 此方法会匹配单一对象,如果此对象为{@code null}则返回false
+ * 如果此对象为非数组,理解为此对象为数组的第一个元素,则返回true
+ * 如果此对象为数组对象,数组长度大于0情况下返回true,否则返回false + * + * @param array 数组 + * @return 是否为非空 + */ + public static boolean isNotEmpty(final Object array) { + return false == isEmpty((Object) array); + } + + /** + * 数组是否为非空 + * + * @param array 数组 + * @return 是否为非空 + */ + public static boolean isNotEmpty(final long... array) { + return (array != null && array.length != 0); + } + + /** + * 数组是否为非空 + * + * @param array 数组 + * @return 是否为非空 + */ + public static boolean isNotEmpty(final int... array) { + return (array != null && array.length != 0); + } + + /** + * 数组是否为非空 + * + * @param array 数组 + * @return 是否为非空 + */ + public static boolean isNotEmpty(final short... array) { + return (array != null && array.length != 0); + } + + /** + * 数组是否为非空 + * + * @param array 数组 + * @return 是否为非空 + */ + public static boolean isNotEmpty(final char... array) { + return (array != null && array.length != 0); + } + + /** + * 数组是否为非空 + * + * @param array 数组 + * @return 是否为非空 + */ + public static boolean isNotEmpty(final byte... array) { + return (array != null && array.length != 0); + } + + /** + * 数组是否为非空 + * + * @param array 数组 + * @return 是否为非空 + */ + public static boolean isNotEmpty(final double... array) { + return (array != null && array.length != 0); + } + + /** + * 数组是否为非空 + * + * @param array 数组 + * @return 是否为非空 + */ + public static boolean isNotEmpty(final float... array) { + return (array != null && array.length != 0); + } + + /** + * 数组是否为非空 + * + * @param array 数组 + * @return 是否为非空 + */ + public static boolean isNotEmpty(final boolean... array) { + return (array != null && array.length != 0); + } + + /** + * 是否包含{@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 (null == element) { + return true; + } + } + } + return false; + } + + /** + * 返回数组中第一个非空元素 + * + * @param 数组元素类型 + * @param array 数组 + * @return 非空元素,如果不存在非空元素或数组为空,返回{@code null} + * @since 3.0.7 + */ + @SuppressWarnings("unchecked") + public static T firstNonNull(T... array) { + if (isNotEmpty(array)) { + for (final T val : array) { + if (null != val) { + return val; + } + } + } + return null; + } + + /** + * 新建一个空数组 + * + * @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 (false == 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 { + 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); + } + } + + /** + * 将新元素插入到到已有数组中的某个位置
+ * 添加新元素会生成一个新的数组,不影响原数组
+ * 如果插入位置为为负数,从原数组从后向前计数,若大于原数组长度,则空白处用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") + 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 T[] result = newArray(array.getClass().getComponentType(), Math.max(len, index) + newElements.length); + System.arraycopy(array, 0, result, 0, Math.min(len, index)); + System.arraycopy(newElements, 0, result, index, newElements.length); + if (index < len) { + System.arraycopy(array, index, result, index + newElements.length, len - index); + } + return result; + } + + /** + * 生成一个新的重新设置大小的数组
+ * 调整大小后拷贝原数组到新数组下。扩大则占位前N个位置,缩小则截断 + * + * @param 数组元素类型 + * @param buffer 原数组 + * @param newSize 新的数组大小 + * @param componentType 数组元素类型 + * @return 调整后的新数组 + */ + public static T[] resize(T[] buffer, int newSize, Class componentType) { + T[] newArray = newArray(componentType, newSize); + if (isNotEmpty(buffer)) { + System.arraycopy(buffer, 0, newArray, 0, Math.min(buffer.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 (array == null) { + continue; + } + length += array.length; + } + T[] result = newArray(arrays.getClass().getComponentType().getComponentType(), length); + + length = 0; + for (T[] array : arrays) { + if (array == null) { + continue; + } + 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) { + 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) { + 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(); + } + + /** + * 克隆数组,如果非数组返回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; + } + + /** + * 生成一个从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; + } + + /** + * 拆分byte数组为几个等份(最后一份可能小于len) + * + * @param array 数组 + * @param len 每个小节的长度 + * @return 拆分后的数组 + */ + public static byte[][] split(byte[] array, int len) { + int x = array.length / len; + int y = array.length % len; + int z = 0; + if (y != 0) { + z = 1; + } + byte[][] arrays = new byte[x + z][]; + byte[] arr; + for (int i = 0; i < x + z; i++) { + arr = new byte[len]; + if (i == x + z - 1 && y != 0) { + System.arraycopy(array, i * len, arr, 0, y); + } else { + System.arraycopy(array, i * len, arr, 0, len); + } + arrays[i] = arr; + } + return arrays; + } + + /** + * 过滤
+ * 过滤过程通过传入的Editor实现来返回需要的元素内容,这个Editor实现可以实现以下功能: + * + *
+	 * 1、过滤出需要的对象,如果返回null表示这个元素对象抛弃
+	 * 2、修改元素对象,返回集合中为修改后的对象
+	 * 
+ * + * @param 数组元素类型 + * @param array 数组 + * @param editor 编辑器接口 + * @return 过滤后的数组 + */ + public static T[] filter(T[] array, Editor editor) { + ArrayList list = new ArrayList(array.length); + T modified; + for (T t : array) { + modified = editor.edit(t); + if (null != modified) { + list.add(modified); + } + } + return list.toArray(Arrays.copyOf(array, list.size())); + } + + /** + * 过滤
+ * 过滤过程通过传入的Filter实现来过滤返回需要的元素内容,这个Filter实现可以实现以下功能: + * + *
+	 * 1、过滤出需要的对象,{@link Filter#accept(Object)}方法返回true的对象将被加入结果集合中
+	 * 
+ * + * @param 数组元素类型 + * @param array 数组 + * @param filter 过滤器接口,用于定义过滤规则,null表示不过滤,返回原数组 + * @return 过滤后的数组 + * @since 3.2.1 + */ + public static T[] filter(T[] array, Filter filter) { + if(null == filter) { + return array; + } + + final ArrayList list = new ArrayList(array.length); + for (T t : array) { + if (filter.accept(t)) { + list.add(t); + } + } + final T[] result = newArray(array.getClass().getComponentType(), list.size()); + return list.toArray(result); + } + + /** + * 去除{@code null} 元素 + * + * @param array 数组 + * @return 处理后的数组 + * @since 3.2.2 + */ + public static T[] removeNull(T[] array) { + return filter(array, new Editor() { + @Override + public T edit(T t) { + // 返回null便不加入集合 + return t; + } + }); + } + + /** + * 去除{@code null}或者"" 元素 + * + * @param array 数组 + * @return 处理后的数组 + * @since 3.2.2 + */ + public static T[] removeEmpty(T[] array) { + return filter(array, new Filter() { + @Override + public boolean accept(T t) { + return false == StrUtil.isEmpty(t); + } + }); + } + + /** + * 去除{@code null}或者""或者空白字符串 元素 + * + * @param array 数组 + * @return 处理后的数组 + * @since 3.2.2 + */ + public static T[] removeBlank(T[] array) { + return filter(array, new Filter() { + @Override + public boolean accept(T t) { + return false == StrUtil.isBlank(t); + } + }); + } + + /** + * 数组元素中的null转换为"" + * + * @param array 数组 + * @return 新数组 + * @since 3.2.1 + */ + public static String[] nullToEmpty(String[] array) { + return filter(array, new Editor() { + @Override + public String edit(String t) { + return 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 = CollectionUtil.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 被检查的元素 + * @return 数组中指定元素所在位置,未找到返回{@link #INDEX_NOT_FOUND} + * @since 3.0.7 + */ + public static int indexOf(T[] array, Object value) { + if (null != array) { + for (int i = 0; i < array.length; i++) { + if (ObjectUtil.equal(value, array[i])) { + return i; + } + } + } + return INDEX_NOT_FOUND; + } + + /** + * 返回数组中指定元素所在位置,忽略大小写,未找到返回{@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 (null != array) { + for (int i = array.length - 1; 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 array 数组 + * @param value 被检查的元素 + * @return 是否包含 + * @since 3.1.2 + */ + public static boolean containsIgnoreCase(CharSequence[] array, CharSequence value) { + return indexOfIgnoreCase(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(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 (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 (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 (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 (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] = Integer.valueOf(values[i]); + } + return array; + } + + /** + * 包装类数组转为原始类型数组 + * + * @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] = values[i].intValue(); + } + 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] = Long.valueOf(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] = values[i].longValue(); + } + 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] = Character.valueOf(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] = values[i].charValue(); + } + 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] = Byte.valueOf(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] = values[i].byteValue(); + } + 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] = Short.valueOf(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] = values[i].shortValue(); + } + 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] = Float.valueOf(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] = values[i].floatValue(); + } + 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] = Double.valueOf(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] = values[i].doubleValue(); + } + 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] = Boolean.valueOf(values[i]); + } + return array; + } + + /** + * 包装类数组转为原始类型数组 + * + * @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] = values[i].booleanValue(); + } + return array; + } + + /** + * 包装数组对象 + * + * @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) { + if (null == obj) { + // throw new NullPointerException("Object check for isArray is null"); + return false; + } + return 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 数组 + * @param indexes 下标列表 + * @return 结果 + */ + public static T[] getAny(Object array, int... indexes) { + if(null == array) { + return null; + } + + final T[] result = newArray(array.getClass().getComponentType(), indexes.length); + for (int i : indexes) { + result[i] = get(array, i); + } + return result; + } + + /** + * 获取子数组 + * + * @param array 数组 + * @param start 开始位置(包括) + * @param end 结束位置(不包括) + * @return 新的数组 + * @since 4.2.2 + * @see Arrays#copyOfRange(Object[], int, int) + */ + 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.5.2 + * @see Arrays#copyOfRange(Object[], int, int) + */ + public static byte[] sub(byte[] array, int start, int end) { + int length = length(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 新的数组 + * @since 4.5.2 + * @see Arrays#copyOfRange(Object[], int, int) + */ + public static int[] sub(int[] array, int start, int end) { + int length = length(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 新的数组 + * @since 4.5.2 + * @see Arrays#copyOfRange(Object[], int, int) + */ + public static long[] sub(long[] array, int start, int end) { + int length = length(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 新的数组 + * @since 4.5.2 + * @see Arrays#copyOfRange(Object[], int, int) + */ + public static short[] sub(short[] array, int start, int end) { + int length = length(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 新的数组 + * @since 4.5.2 + * @see Arrays#copyOfRange(Object[], int, int) + */ + public static char[] sub(char[] array, int start, int end) { + int length = length(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 新的数组 + * @since 4.5.2 + * @see Arrays#copyOfRange(Object[], int, int) + */ + public static double[] sub(double[] array, int start, int end) { + int length = length(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 新的数组 + * @since 4.5.2 + * @see Arrays#copyOfRange(Object[], int, int) + */ + public static float[] sub(float[] array, int start, int end) { + int length = length(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 新的数组 + * @since 4.5.2 + * @see Arrays#copyOfRange(Object[], int, int) + */ + public static boolean[] sub(boolean[] array, int start, int end) { + int length = length(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); + } + + /** + * 获取子数组 + * + * @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 (ArrayUtil.isArray(obj)) { + try { + return Arrays.deepToString((Object[]) obj); + } catch (Exception e) { + final String className = obj.getClass().getComponentType().getName(); + switch (className) { + case "long": + return Arrays.toString((long[]) obj); + case "int": + return Arrays.toString((int[]) obj); + case "short": + return Arrays.toString((short[]) obj); + case "char": + return Arrays.toString((char[]) obj); + case "byte": + return Arrays.toString((byte[]) obj); + case "boolean": + return Arrays.toString((boolean[]) obj); + case "float": + return Arrays.toString((float[]) obj); + case "double": + return Arrays.toString((double[]) obj); + default: + throw new UtilException(e); + } + } + } + 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 如果参数不为数组,抛出此异常 + * @since 3.0.8 + * @see Array#getLength(Object) + */ + 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 conjunction 分隔符 + * @param prefix 每个元素添加的前缀,null表示不添加 + * @param suffix 每个元素添加的后缀,null表示不添加 + * @return 连接后的字符串 + * @since 4.0.10 + */ + public static String join(T[] array, CharSequence conjunction, String prefix, String suffix) { + if (null == array) { + return null; + } + + final StringBuilder sb = new StringBuilder(); + boolean isFirst = true; + for (T item : array) { + if (isFirst) { + isFirst = false; + } else { + sb.append(conjunction); + } + if (ArrayUtil.isArray(item)) { + sb.append(join(ArrayUtil.wrap(item), conjunction, prefix, suffix)); + } else if (item instanceof Iterable) { + sb.append(IterUtil.join((Iterable) item, conjunction, prefix, suffix)); + } else if (item instanceof Iterator) { + sb.append(IterUtil.join((Iterator) item, conjunction, prefix, suffix)); + } else { + sb.append(StrUtil.wrap(StrUtil.toString(item), prefix, suffix)); + } + } + return sb.toString(); + } + + /** + * 以 conjunction 为分隔符将数组转换为字符串 + * + * @param array 数组 + * @param conjunction 分隔符 + * @return 连接后的字符串 + */ + public static String join(long[] array, CharSequence conjunction) { + if (null == array) { + return null; + } + + final StringBuilder sb = new StringBuilder(); + boolean isFirst = true; + for (long item : array) { + if (isFirst) { + isFirst = false; + } else { + sb.append(conjunction); + } + sb.append(item); + } + return sb.toString(); + } + + /** + * 以 conjunction 为分隔符将数组转换为字符串 + * + * @param array 数组 + * @param conjunction 分隔符 + * @return 连接后的字符串 + */ + public static String join(int[] array, CharSequence conjunction) { + if (null == array) { + return null; + } + + final StringBuilder sb = new StringBuilder(); + boolean isFirst = true; + for (int item : array) { + if (isFirst) { + isFirst = false; + } else { + sb.append(conjunction); + } + sb.append(item); + } + return sb.toString(); + } + + /** + * 以 conjunction 为分隔符将数组转换为字符串 + * + * @param array 数组 + * @param conjunction 分隔符 + * @return 连接后的字符串 + */ + public static String join(short[] array, CharSequence conjunction) { + if (null == array) { + return null; + } + + final StringBuilder sb = new StringBuilder(); + boolean isFirst = true; + for (short item : array) { + if (isFirst) { + isFirst = false; + } else { + sb.append(conjunction); + } + sb.append(item); + } + return sb.toString(); + } + + /** + * 以 conjunction 为分隔符将数组转换为字符串 + * + * @param array 数组 + * @param conjunction 分隔符 + * @return 连接后的字符串 + */ + public static String join(char[] array, CharSequence conjunction) { + if (null == array) { + return null; + } + + final StringBuilder sb = new StringBuilder(); + boolean isFirst = true; + for (char item : array) { + if (isFirst) { + isFirst = false; + } else { + sb.append(conjunction); + } + sb.append(item); + } + return sb.toString(); + } + + /** + * 以 conjunction 为分隔符将数组转换为字符串 + * + * @param array 数组 + * @param conjunction 分隔符 + * @return 连接后的字符串 + */ + public static String join(byte[] array, CharSequence conjunction) { + if (null == array) { + return null; + } + + final StringBuilder sb = new StringBuilder(); + boolean isFirst = true; + for (byte item : array) { + if (isFirst) { + isFirst = false; + } else { + sb.append(conjunction); + } + sb.append(item); + } + return sb.toString(); + } + + /** + * 以 conjunction 为分隔符将数组转换为字符串 + * + * @param array 数组 + * @param conjunction 分隔符 + * @return 连接后的字符串 + */ + public static String join(boolean[] array, CharSequence conjunction) { + if (null == array) { + return null; + } + + final StringBuilder sb = new StringBuilder(); + boolean isFirst = true; + for (boolean item : array) { + if (isFirst) { + isFirst = false; + } else { + sb.append(conjunction); + } + sb.append(item); + } + return sb.toString(); + } + + /** + * 以 conjunction 为分隔符将数组转换为字符串 + * + * @param array 数组 + * @param conjunction 分隔符 + * @return 连接后的字符串 + */ + public static String join(float[] array, CharSequence conjunction) { + if (null == array) { + return null; + } + + final StringBuilder sb = new StringBuilder(); + boolean isFirst = true; + for (float item : array) { + if (isFirst) { + isFirst = false; + } else { + sb.append(conjunction); + } + sb.append(item); + } + return sb.toString(); + } + + /** + * 以 conjunction 为分隔符将数组转换为字符串 + * + * @param array 数组 + * @param conjunction 分隔符 + * @return 连接后的字符串 + */ + public static String join(double[] array, CharSequence conjunction) { + if (null == array) { + return null; + } + + final StringBuilder sb = new StringBuilder(); + boolean isFirst = true; + for (double item : array) { + if (isFirst) { + isFirst = false; + } else { + sb.append(conjunction); + } + sb.append(item); + } + return sb.toString(); + } + + /** + * 以 conjunction 为分隔符将数组转换为字符串 + * + * @param array 数组 + * @param conjunction 分隔符 + * @return 连接后的字符串 + */ + public static String join(Object array, CharSequence conjunction) { + if (isArray(array)) { + final Class componentType = array.getClass().getComponentType(); + if (componentType.isPrimitive()) { + final String componentTypeName = componentType.getName(); + switch (componentTypeName) { + case "long": + return join((long[]) array, conjunction); + case "int": + return join((int[]) array, conjunction); + case "short": + return join((short[]) array, conjunction); + case "char": + return join((char[]) array, conjunction); + case "byte": + return join((byte[]) array, conjunction); + case "boolean": + return join((boolean[]) array, conjunction); + case "float": + return join((float[]) array, conjunction); + case "double": + return join((double[]) array, conjunction); + default: + throw new UtilException("Unknown primitive type: [{}]", componentTypeName); + } + } else { + return join((Object[]) array, conjunction); + } + } + throw new UtilException(StrUtil.format("[{}] is not a Array!", array.getClass())); + } + + /** + * {@link ByteBuffer} 转byte数组 + * + * @param bytebuffer {@link ByteBuffer} + * @return byte数组 + * @since 3.0.1 + */ + public static byte[] toArray(ByteBuffer bytebuffer) { + if (false == bytebuffer.hasArray()) { + int oldPosition = bytebuffer.position(); + bytebuffer.position(0); + int size = bytebuffer.limit(); + byte[] buffers = new byte[size]; + bytebuffer.get(buffers); + bytebuffer.position(oldPosition); + return buffers; + } else { + return Arrays.copyOfRange(bytebuffer.array(), bytebuffer.position(), bytebuffer.limit()); + } + } + + /** + * 将集合转为数组 + * + * @param 数组元素类型 + * @param iterator {@link Iterator} + * @param componentType 集合元素类型 + * @return 数组 + * @since 3.0.9 + */ + public static T[] toArray(Iterator iterator, Class componentType) { + return toArray(CollectionUtil.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) { + final T[] array = newArray(componentType, collection.size()); + return collection.toArray(array); + } + + // ---------------------------------------------------------------------- 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); + } + + /** + * 移除数组中对应位置的元素
+ * 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 + */ + public static Object remove(Object array, int index) throws IllegalArgumentException { + if (null == array) { + return array; + } + int length = length(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; + } + + // ---------------------------------------------------------------------- remove + /** + * 移除数组中指定的元素
+ * 只会移除匹配到的第一个元素 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)); + } + + /** + * 移除数组中指定的元素
+ * 只会移除匹配到的第一个元素 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 array + + /** + * 反转数组,会变更原数组 + * + * @param 数组元素类型 + * @param array 数组,会变更 + * @param startIndexInclusive 其实位置(包含) + * @param endIndexExclusive 结束位置(不包含) + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static T[] reverse(final T[] array, final int startIndexInclusive, final int endIndexExclusive) { + if (isEmpty(array)) { + return array; + } + int i = startIndexInclusive < 0 ? 0 : startIndexInclusive; + 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(final T[] array) { + return reverse(array, 0, array.length); + } + + /** + * 反转数组,会变更原数组 + * + * @param array 数组,会变更 + * @param startIndexInclusive 其实位置(包含) + * @param endIndexExclusive 结束位置(不包含) + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static long[] reverse(final long[] array, final int startIndexInclusive, final int endIndexExclusive) { + if (isEmpty(array)) { + return array; + } + int i = startIndexInclusive < 0 ? 0 : startIndexInclusive; + int j = Math.min(array.length, endIndexExclusive) - 1; + long tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + return array; + } + + /** + * 反转数组,会变更原数组 + * + * @param array 数组,会变更 + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static long[] reverse(final long[] array) { + return reverse(array, 0, array.length); + } + + /** + * 反转数组,会变更原数组 + * + * @param array 数组,会变更 + * @param startIndexInclusive 其实位置(包含) + * @param endIndexExclusive 结束位置(不包含) + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static int[] reverse(final int[] array, final int startIndexInclusive, final int endIndexExclusive) { + if (isEmpty(array)) { + return array; + } + int i = startIndexInclusive < 0 ? 0 : startIndexInclusive; + int j = Math.min(array.length, endIndexExclusive) - 1; + int tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + return array; + } + + /** + * 反转数组,会变更原数组 + * + * @param array 数组,会变更 + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static int[] reverse(final int[] array) { + return reverse(array, 0, array.length); + } + + /** + * 反转数组,会变更原数组 + * + * @param array 数组,会变更 + * @param startIndexInclusive 其实位置(包含) + * @param endIndexExclusive 结束位置(不包含) + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static short[] reverse(final short[] array, final int startIndexInclusive, final int endIndexExclusive) { + if (isEmpty(array)) { + return array; + } + int i = startIndexInclusive < 0 ? 0 : startIndexInclusive; + int j = Math.min(array.length, endIndexExclusive) - 1; + short tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + return array; + } + + /** + * 反转数组,会变更原数组 + * + * @param array 数组,会变更 + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static short[] reverse(final short[] array) { + return reverse(array, 0, array.length); + } + + /** + * 反转数组,会变更原数组 + * + * @param array 数组,会变更 + * @param startIndexInclusive 其实位置(包含) + * @param endIndexExclusive 结束位置(不包含) + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static char[] reverse(final char[] array, final int startIndexInclusive, final int endIndexExclusive) { + if (isEmpty(array)) { + return array; + } + int i = startIndexInclusive < 0 ? 0 : startIndexInclusive; + int j = Math.min(array.length, endIndexExclusive) - 1; + char tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + return array; + } + + /** + * 反转数组,会变更原数组 + * + * @param array 数组,会变更 + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static char[] reverse(final char[] array) { + return reverse(array, 0, array.length); + } + + /** + * 反转数组,会变更原数组 + * + * @param array 数组,会变更 + * @param startIndexInclusive 其实位置(包含) + * @param endIndexExclusive 结束位置(不包含) + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static byte[] reverse(final byte[] array, final int startIndexInclusive, final int endIndexExclusive) { + if (isEmpty(array)) { + return array; + } + int i = startIndexInclusive < 0 ? 0 : startIndexInclusive; + int j = Math.min(array.length, endIndexExclusive) - 1; + byte tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + return array; + } + + /** + * 反转数组,会变更原数组 + * + * @param array 数组,会变更 + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static byte[] reverse(final byte[] array) { + return reverse(array, 0, array.length); + } + + /** + * 反转数组,会变更原数组 + * + * @param array 数组,会变更 + * @param startIndexInclusive 其实位置(包含) + * @param endIndexExclusive 结束位置(不包含) + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static double[] reverse(final double[] array, final int startIndexInclusive, final int endIndexExclusive) { + if (isEmpty(array)) { + return array; + } + int i = startIndexInclusive < 0 ? 0 : startIndexInclusive; + int j = Math.min(array.length, endIndexExclusive) - 1; + double tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + return array; + } + + /** + * 反转数组,会变更原数组 + * + * @param array 数组,会变更 + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static double[] reverse(final double[] array) { + return reverse(array, 0, array.length); + } + + /** + * 反转数组,会变更原数组 + * + * @param array 数组,会变更 + * @param startIndexInclusive 其实位置(包含) + * @param endIndexExclusive 结束位置(不包含) + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static float[] reverse(final float[] array, final int startIndexInclusive, final int endIndexExclusive) { + if (isEmpty(array)) { + return array; + } + int i = startIndexInclusive < 0 ? 0 : startIndexInclusive; + int j = Math.min(array.length, endIndexExclusive) - 1; + float tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + return array; + } + + /** + * 反转数组,会变更原数组 + * + * @param array 数组,会变更 + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static float[] reverse(final float[] array) { + return reverse(array, 0, array.length); + } + + /** + * 反转数组,会变更原数组 + * + * @param array 数组,会变更 + * @param startIndexInclusive 其实位置(包含) + * @param endIndexExclusive 结束位置(不包含) + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static boolean[] reverse(final boolean[] array, final int startIndexInclusive, final int endIndexExclusive) { + if (isEmpty(array)) { + return array; + } + int i = startIndexInclusive < 0 ? 0 : startIndexInclusive; + int j = Math.min(array.length, endIndexExclusive) - 1; + boolean tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + return array; + } + + /** + * 反转数组,会变更原数组 + * + * @param array 数组,会变更 + * @return 变更后的原数组 + * @since 3.0.9 + */ + public static boolean[] reverse(final boolean[] array) { + return reverse(array, 0, array.length); + } + + // ------------------------------------------------------------------------------------------------------------ min and max + /** + * 取最小值 + * + * @param 元素类型 + * @param numberArray 数字数组 + * @return 最小值 + * @since 3.0.9 + */ + public static > T min(T[] numberArray) { + if (isEmpty(numberArray)) { + throw new IllegalArgumentException("Number array must not empty !"); + } + T min = numberArray[0]; + for (int i = 0; i < numberArray.length; i++) { + if (ObjectUtil.compare(min, numberArray[i]) > 0) { + min = numberArray[i]; + } + } + return min; + } + + /** + * 取最小值 + * + * @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 = 0; 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 = 0; 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 = 0; 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 = 0; 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 = 0; 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 = 0; 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 = 0; i < numberArray.length; i++) { + if (min > numberArray[i]) { + min = numberArray[i]; + } + } + return min; + } + + /** + * 取最大值 + * + * @param 元素类型 + * @param numberArray 数字数组 + * @return 最大值 + * @since 3.0.9 + */ + public static > T max(T[] numberArray) { + if (isEmpty(numberArray)) { + throw new IllegalArgumentException("Number array must not empty !"); + } + T max = numberArray[0]; + for (int i = 0; i < numberArray.length; i++) { + if (ObjectUtil.compare(max, numberArray[i]) < 0) { + max = numberArray[i]; + } + } + return max; + } + + /** + * 取最大值 + * + * @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 = 0; 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 = 0; 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 = 0; 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 = 0; 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 = 0; 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 = 0; 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 = 0; i < numberArray.length; i++) { + if (max < numberArray[i]) { + max = numberArray[i]; + } + } + return max; + } + + /** + * 交换数组中两个位置的值 + * + * @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; + } + + /** + * 交换数组中两个位置的值 + * + * @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)) { + return 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) { + return emptyCount(args) == args.length; + } + + /** + * 是否存都不为{@code null}或空对象,通过{@link ObjectUtil#isEmpty(Object)} 判断元素 + * + * @param args 被检查的对象,一个或者多个 + * @return 是否都不为空 + * @since 4.5.18 + */ + public static boolean isAllNotEmpty(Object... args) { + return false == hasEmpty(args); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/util/BooleanUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/BooleanUtil.java new file mode 100644 index 000000000..d229cbc3b --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/util/BooleanUtil.java @@ -0,0 +1,447 @@ +package cn.hutool.core.util; + +import cn.hutool.core.convert.Convert; + +/** + * Boolean类型相关工具类 + * + * @author looly + * @since 4.1.16 + */ +public class BooleanUtil { + + /** 表示为真的字符串 */ + private static final String[] TRUE_ARRAY = { "true", "yes", "y", "t", "ok", "1", "on", "是", "对", "真", }; + + /** + * 取相反值 + * + * @param bool Boolean值 + * @return 相反的Boolean值 + */ + public static Boolean negate(Boolean bool) { + if (bool == null) { + return null; + } + return bool.booleanValue() ? 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 ? false : true; + } + + /** + * 转换字符串为boolean值 + * + * @param valueStr 字符串 + * @return boolean值 + */ + public static boolean toBoolean(String valueStr) { + if (StrUtil.isNotBlank(valueStr)) { + valueStr = valueStr.trim().toLowerCase(); + if (ArrayUtil.contains(TRUE_ARRAY, valueStr)) { + return true; + } + } + return false; + } + + /** + * 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 Integer.valueOf(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 Character.valueOf(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 Byte.valueOf(toByte(value)); + } + + /** + * boolean值转为long + * + * @param value Boolean值 + * @return long值 + */ + public static long toLong(boolean value) { + return (long) toInt(value); + } + + /** + * boolean值转为Long + * + * @param value Boolean值 + * @return Long值 + */ + public static Long toLongObj(boolean value) { + return Long.valueOf(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 Short.valueOf(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 Float.valueOf(toFloat(value)); + } + + /** + * boolean值转为double + * + * @param value Boolean值 + * @return double值 + */ + public static double toDouble(boolean value) { + return (double) toInt(value); + } + + /** + * boolean值转为double + * + * @param value Boolean值 + * @return double值 + */ + public static Double toDoubleObj(boolean value) { + return Double.valueOf(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.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 (false == 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 and(final Boolean... array) { + if (ArrayUtil.isEmpty(array)) { + throw new IllegalArgumentException("The Array must not be empty !"); + } + final boolean[] primitive = Convert.convert(boolean[].class, array); + return Boolean.valueOf(and(primitive)); + } + + /** + * 对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 or(Boolean... array) { + if (ArrayUtil.isEmpty(array)) { + throw new IllegalArgumentException("The Array must not be empty !"); + } + final boolean[] primitive = Convert.convert(boolean[].class, array); + return Boolean.valueOf(or(primitive)); + } + + /** + * 对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 xor(Boolean... array) { + if (ArrayUtil.isEmpty(array)) { + throw new IllegalArgumentException("The Array must not be empty !"); + } + final boolean[] primitive = Convert.convert(boolean[].class, array); + return Boolean.valueOf(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/hutool-core/src/main/java/cn/hutool/core/util/CharUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/CharUtil.java new file mode 100644 index 000000000..1ec229804 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/util/CharUtil.java @@ -0,0 +1,322 @@ +package cn.hutool.core.util; + +import cn.hutool.core.text.ASCIIStrCache; + +/** + * 字符工具类
+ * 部分工具来自于Apache Commons系列 + * + * @author looly + * @since 4.0.1 + */ +public class CharUtil { + + public static final char SPACE = ' '; + public static final char TAB = ' '; + public static final char DOT = '.'; + public static final char SLASH = '/'; + public static final char BACKSLASH = '\\'; + public static final char CR = '\r'; + public static final char LF = '\n'; + public static final char UNDERLINE = '_'; + public static final char DASHED = '-'; + public static final char COMMA = ','; + public static final char DELIM_START = '{'; + public static final char DELIM_END = '}'; + public static final char BRACKET_START = '['; + public static final char BRACKET_END = ']'; + public static final char COLON = ':'; + public static final char DOUBLE_QUOTES = '"'; + public static final char SINGLE_QUOTE = '\''; + public static final char AMP = '&'; + + /** + * 是否为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) { + 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); + } + + /** + * 是否空白符
+ * 空白符包括空格、制表符、全角空格和不间断空格
+ * + * @see Character#isWhitespace(int) + * @see Character#isSpaceChar(int) + * @param c 字符 + * @return 是否空白符 + * @since 4.0.10 + */ + public static boolean isBlankChar(int c) { + return Character.isWhitespace(c) || Character.isSpaceChar(c) || c == '\ufeff' || c == '\u202a'; + } + + /** + * 判断是否为emoji表情符
+ * + * @param c 字符 + * @return 是否为emoji + * @since 4.0.8 + */ + public static boolean isEmoji(char c) { + return false == ((c == 0x0) || // + (c == 0x9) || // + (c == 0xA) || // + (c == 0xD) || // + ((c >= 0x20) && (c <= 0xD7FF)) || // + ((c >= 0xE000) && (c <= 0xFFFD)) || // + ((c >= 0x10000) && (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 ignoreCase 是否忽略大小写 + * @return 是否相同 + * @since 4.0.3 + */ + public static boolean equals(char c1, char c2, boolean ignoreCase) { + if (ignoreCase) { + return Character.toLowerCase(c1) == Character.toLowerCase(c2); + } + return c1 == c2; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/util/CharsetUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/CharsetUtil.java new file mode 100644 index 000000000..2a5e3b213 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/util/CharsetUtil.java @@ -0,0 +1,136 @@ +package cn.hutool.core.util; + +import java.io.File; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.charset.UnsupportedCharsetException; + +import cn.hutool.core.io.FileUtil; + +/** + * 字符集工具类 + * @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 = Charset.forName(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); + } + + /** + * 转换字符串的字符集编码 + * @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()} + * + * @see CharsetUtil#defaultCharsetName() + * @return 系统字符集编码 + * @since 3.1.2 + */ + public static String systemCharsetName() { + return systemCharset().name(); + } + + /** + * 系统字符集编码,如果是Windows,则默认为GBK编码,否则取 {@link CharsetUtil#defaultCharsetName()} + * + * @see CharsetUtil#defaultCharsetName() + * @return 系统字符集编码 + * @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(); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/util/ClassLoaderUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/ClassLoaderUtil.java new file mode 100644 index 000000000..f0dd330ca --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/util/ClassLoaderUtil.java @@ -0,0 +1,293 @@ +package cn.hutool.core.util; + +import java.io.File; +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +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.lang.SimpleCache; + +/** + * {@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> primitiveTypeNameMap = new ConcurrentHashMap>(32); + private static SimpleCache> classCache = new SimpleCache<>(); + + static { + List> primitiveTypes = new ArrayList>(32); + // 加入原始类型 + primitiveTypes.addAll(BasicType.primitiveWrapperMap.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 (Class primitiveType : primitiveTypes) { + primitiveTypeNameMap.put(primitiveType.getName(), primitiveType); + } + } + + /** + * 获取当前线程的{@link ClassLoader} + * + * @return 当前线程的class loader + * @see Thread#getContextClassLoader() + */ + public static ClassLoader getContextClassLoader() { + return Thread.currentThread().getContextClassLoader(); + } + + /** + * 获取{@link ClassLoader}
+ * 获取顺序如下:
+ * + *
+	 * 1、获取当前线程的ContextClassLoader
+	 * 2、获取{@link ClassLoaderUtil}类对应的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 = 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} 则使用系统默认ClassLoader + * @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"); + + // 加载原始类型和缓存中的类 + Class clazz = loadPrimitiveClass(name); + if (clazz == null) { + clazz = classCache.get(name); + } + if (clazz != null) { + return 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 classCache.put(name, 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 = primitiveTypeNameMap.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 + /** + * 尝试转换并加载内部类,例如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/hutool-core/src/main/java/cn/hutool/core/util/ClassUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/ClassUtil.java new file mode 100644 index 000000000..ad8c3527f --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/util/ClassUtil.java @@ -0,0 +1,1021 @@ +package cn.hutool.core.util; + +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.util.Date; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +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.ClassScaner; +import cn.hutool.core.lang.Filter; +import cn.hutool.core.lang.Singleton; + +/** + * 类工具类
+ * + * @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]; + classes[i] = (null == obj) ? Object.class : 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 ClassScaner#scanPackageByAnnotation(String, Class) + */ + public static Set> scanPackageByAnnotation(String packageName, final Class annotationClass) { + return ClassScaner.scanPackageByAnnotation(packageName, annotationClass); + } + + /** + * 扫描指定包路径下所有指定类或接口的子类或实现类 + * + * @param packageName 包路径 + * @param superClass 父类或接口 + * @return 类集合 + * @see ClassScaner#scanPackageBySuper(String, Class) + */ + public static Set> scanPackageBySuper(String packageName, final Class superClass) { + return ClassScaner.scanPackageBySuper(packageName, superClass); + } + + /** + * 扫面该包路径下所有class文件 + * + * @return 类集合 + * @see ClassScaner#scanPackage() + */ + public static Set> scanPackage() { + return ClassScaner.scanPackage(); + } + + /** + * 扫面该包路径下所有class文件 + * + * @param packageName 包路径 com | com. | com.abs | com.abs. + * @return 类集合 + * @see ClassScaner#scanPackage(String) + */ + public static Set> scanPackage(String packageName) { + return ClassScaner.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 ClassScaner.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 getPublicMethods(clazz, excludeMethodNames); + } + + /** + * 查找指定Public方法 如果找不到对应的方法或方法不为public的则返回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类的方法 找不到方法会返回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字段), 字段不存在则返回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 ClassUtil}类对应的ClassLoader
+	 * 3、获取系统ClassLoader({@link ClassLoader#getSystemClassLoader()})
+	 * 
+ * + * @return 类加载器 + */ + public static ClassLoader getClassLoader() { + return ClassLoaderUtil.getClassLoader(); + } + + /** + * 比较判断types1和types2两组类,如果types1中所有的类都与types2对应位置的类相同,或者是其父类或接口,则返回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 (false == 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 类名和方法名表达式,类名与方法名用.#连接 例如: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.wrapperPrimitiveMap.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, a Number, a Date, a URI, a URL, a Locale or a 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);// + } + + /** + * 检查目标类是否可以从原类转化
+ * 转化包括:
+ * 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.wrapperPrimitiveMap.get(sourceType); + if (resolvedPrimitive != null && targetType.equals(resolvedPrimitive)) { + return true; + } + } else { + // 包装类型 + Class resolvedWrapper = BasicType.primitiveWrapperMap.get(sourceType); + if (resolvedWrapper != null && targetType.isAssignableFrom(resolvedWrapper)) { + return true; + } + } + return false; + } + + /** + * 指定类是否为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 false == isPublic(clazz); + } + + /** + * 指定方法是否为非public + * + * @param method 方法 + * @return 是否为非public + */ + public static boolean isNotPublic(Method method) { + return false == 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 && false == 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 // + && false == clazz.isInterface() // + && false == isAbstract(clazz) // + && false == clazz.isEnum() // + && false == clazz.isArray() // + && false == clazz.isAnnotation() // + && false == clazz.isSynthetic() // + && false == clazz.isPrimitive();// + } + + /** + * 判断类是否为枚举类型 + * + * @param clazz 类 + * @return 是否为枚举类型 + * @since 3.2.0 + */ + public static boolean isEnum(Class clazz) { + return null == clazz ? false : 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); + if (null != argumentType && argumentType instanceof Class) { + return (Class) argumentType; + } + return null; + } + + /** + * 获得给定类所在包的名称
+ * 例如:
+ * 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()) { + 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; + } +} \ No newline at end of file diff --git a/hutool-core/src/main/java/cn/hutool/core/util/EnumUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/EnumUtil.java new file mode 100644 index 000000000..a9098f02f --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/util/EnumUtil.java @@ -0,0 +1,279 @@ +package cn.hutool.core.util; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; + +/** + * 枚举工具类 + * + * @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; + } + + /** + * 字符串转枚举,调用{@link Enum#valueOf(Class, String)} + * + * @param 枚举类型泛型 + * @param enumClass 枚举类 + * @param value 值 + * @return 枚举值 + * @since 4.1.13 + */ + public static > T 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 > T fromString(Class enumClass, String value, T 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 > T 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 enumClass 枚举类 + * @param value 值 + * @return 匹配到的枚举对象,未匹配到返回null + */ + @SuppressWarnings("unchecked") + public static > T 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 (T) 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 (false == names.contains(name)) { + names.add(name); + } + } + return names; + } + + /** + * 获取枚举字符串值和枚举对象的Map对应,使用LinkedHashMap保证有序
+ * 结果中键为枚举名,值为枚举对象 + * + * @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); + for (Enum e : enums) { + map.put(e.name(), ReflectUtil.getFieldValue(e, fieldName)); + } + return map; + } + + /** + * 判断某个值是存在枚举中 + * + * @param enumClass 枚举类 + * @param val 需要查找的值 + * @param + * @return 是否存在 + */ + public static > boolean contains(final Class enumClass, String val) { + return EnumUtil.getEnumMap(enumClass).containsKey(val); + } + + /** + * 判断某个值是不存在枚举中 + * + * @param enumClass 枚举类 + * @param val 需要查找的值 + * @param + * @return 是否不存在 + */ + public static > boolean notContains(final Class enumClass, String val) { + return false == 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/hutool-core/src/main/java/cn/hutool/core/util/EscapeUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/EscapeUtil.java new file mode 100644 index 000000000..5bb0e46b7 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/util/EscapeUtil.java @@ -0,0 +1,129 @@ +package cn.hutool.core.util; + +import cn.hutool.core.text.escape.Html4Escape; +import cn.hutool.core.text.escape.Html4Unescape; + +/** + * 转义和反转义工具类Escape / Unescape
+ * escape采用ISO Latin字符集对指定的字符串进行编码。
+ * 所有的空格符、标点符号、特殊字符以及其他非ASCII字符都将被转化成%xx格式的字符编码(xx等于该字符在字符集表里面的编码的16进制数字)。 + * + * @author xiaoleilu + */ +public class EscapeUtil { + + /** + * 转义HTML4中的特殊字符 + * + * @param html HTML文本 + * @return 转义后的文本 + * @since 4.1.5 + */ + public static String escapeHtml4(String html) { + Html4Escape escape = new Html4Escape(); + return escape.replace(html).toString(); + } + + /** + * 反转义HTML4中的特殊字符 + * + * @param html HTML文本 + * @return 转义后的文本 + * @since 4.1.5 + */ + public static String unescapeHtml4(String html) { + Html4Unescape unescape = new Html4Unescape(); + return unescape.replace(html).toString(); + } + + /** + * Escape编码(Unicode)
+ * 该方法不会对 ASCII 字母和数字进行编码,也不会对下面这些 ASCII 标点符号进行编码: * @ - _ + . / 。其他所有的字符都会被转义序列替换。 + * + * @param content 被转义的内容 + * @return 编码后的字符串 + */ + public static String escape(String content) { + if (StrUtil.isBlank(content)) { + return content; + } + + int i; + char j; + StringBuilder tmp = new StringBuilder(); + tmp.ensureCapacity(content.length() * 6); + + for (i = 0; i < content.length(); i++) { + + j = content.charAt(i); + + if (Character.isDigit(j) || Character.isLowerCase(j) || Character.isUpperCase(j)) { + tmp.append(j); + } else if (j < 256) { + tmp.append("%"); + if (j < 16) { + tmp.append("0"); + } + tmp.append(Integer.toString(j, 16)); + } else { + tmp.append("%u"); + tmp.append(Integer.toString(j, 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, pos = 0; + 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.substring(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/hutool-core/src/main/java/cn/hutool/core/util/HashUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/HashUtil.java new file mode 100644 index 000000000..a19c0c78f --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/util/HashUtil.java @@ -0,0 +1,476 @@ +package cn.hutool.core.util; + +import cn.hutool.core.lang.MurmurHash; + +/** + * 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 + 0]; + } + 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 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 = 0; + + 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 = 0; + + 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 = 0; + + 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 += (3 * i * ucChar * ucChar + 5 * i * ucChar + 7 * 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 += (3 * i * ucChar * ucChar + 5 * i * ucChar + 7 * 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); + } +} \ No newline at end of file diff --git a/hutool-core/src/main/java/cn/hutool/core/util/HexUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/HexUtil.java new file mode 100644 index 000000000..e67c436c3 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/util/HexUtil.java @@ -0,0 +1,385 @@ +package cn.hutool.core.util; + +import java.awt.Color; +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 { + + /** + * 用于建立十六进制字符的输出的小写字符数组 + */ + private static final char[] DIGITS_LOWER = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + /** + * 用于建立十六进制字符的输出的大写字符数组 + */ + private static final char[] DIGITS_UPPER = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + + /** + * 判断给定字符串是否为16进制数
+ * 如果是,需要使用对应数字类型对象的decode方法解码
+ * 例如:{@code Integer.decode}方法解码int类型的16进制数字 + * + * @param value 值 + * @return 是否为16进制 + */ + public static boolean isHexNumber(String value) { + final int index = (value.startsWith("-") ? 1 : 0); + if (value.startsWith("0x", index) || value.startsWith("0X", index) || value.startsWith("#", index)) { + try { + Long.decode(value); + } catch (NumberFormatException e) { + return false; + } + return true; + }else { + return false; + } + } + + // ---------------------------------------------------------------------------------------------------- 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 true 传换成小写格式 , false 传换成大写格式 + * @return 十六进制char[] + */ + public static char[] encodeHex(byte[] data, boolean toLowerCase) { + return encodeHex(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER); + } + + /** + * 将字节数组转换为十六进制字符串 + * + * @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 true 传换成小写格式 , false 传换成大写格式 + * @return 十六进制String + */ + public static String encodeHexStr(byte[] data, boolean toLowerCase) { + return encodeHexStr(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER); + } + + // ---------------------------------------------------------------------------------------------------- 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 decodeHexStr(hexStr.toCharArray(), charset); + } + + /** + * 将十六进制字符数组转换为字符串 + * + * @param hexData 十六进制char[] + * @param charset 编码 + * @return 字符串 + */ + public static String decodeHexStr(char[] hexData, Charset charset) { + return StrUtil.str(decodeHex(hexData), charset); + } + + /** + * 将十六进制字符数组转换为字节数组 + * + * @param hexData 十六进制char[] + * @return byte[] + * @throws RuntimeException 如果源十六进制字符数组是一个奇怪的长度,将抛出运行时异常 + */ + public static byte[] decodeHex(char[] hexData) { + + int len = hexData.length; + + if ((len & 0x01) != 0) { + throw new RuntimeException("Odd number of characters."); + } + + byte[] out = new byte[len >> 1]; + + // two characters form the hex value. + for (int i = 0, j = 0; j < len; i++) { + int f = toDigit(hexData[j], j) << 4; + j++; + f = f | toDigit(hexData[j], j); + j++; + out[i] = (byte) (f & 0xFF); + } + + return out; + } + + /** + * 将十六进制字符串解码为byte[] + * + * @param hexStr 十六进制String + * @return byte[] + */ + public static byte[] decodeHex(String hexStr) { + if (StrUtil.isEmpty(hexStr)) { + return null; + } + return decodeHex(hexStr.toCharArray()); + } + + // ---------------------------------------------------------------------------------------- Color + /** + * 将{@link Color}编码为Hex形式 + * + * @param color {@link Color} + * @return Hex字符串 + * @since 3.0.8 + */ + public static String encodeColor(Color color) { + return encodeColor(color, "#"); + } + + /** + * 将{@link Color}编码为Hex形式 + * + * @param color {@link Color} + * @param prefix 前缀字符串,可以是#、0x等 + * @return Hex字符串 + * @since 3.0.8 + */ + public static String encodeColor(Color color, String prefix) { + final StringBuffer builder = new StringBuffer(prefix); + String colorHex; + colorHex = Integer.toHexString(color.getRed()); + if (1 == colorHex.length()) { + builder.append('0'); + } + builder.append(colorHex); + colorHex = Integer.toHexString(color.getGreen()); + if (1 == colorHex.length()) { + builder.append('0'); + } + builder.append(colorHex); + colorHex = Integer.toHexString(color.getBlue()); + if (1 == colorHex.length()) { + builder.append('0'); + } + builder.append(colorHex); + return builder.toString(); + } + + /** + * 将Hex颜色值转为 + * + * @param hexColor 16进制颜色值,可以以#开头,也可以用0x开头 + * @return {@link Color} + * @since 3.0.8 + */ + public static Color decodeColor(String hexColor) { + return Color.decode(hexColor); + } + + /** + * 将指定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) { + StringBuilder sb = new StringBuilder(6); + sb.append("\\u"); + sb.append(DIGITS_LOWER[(ch >> 12) & 15]); + sb.append(DIGITS_LOWER[(ch >> 8) & 15]); + sb.append(DIGITS_LOWER[(ch >> 4) & 15]); + sb.append(DIGITS_LOWER[(ch) & 15]); + return sb.toString(); + } + + /** + * 转为16进制字符串 + * + * @param value int值 + * @return 16进制字符串 + * @since 4.4.1 + */ + public static String toHex(int value) { + return Integer.toHexString(value); + } + + /** + * 转为16进制字符串 + * + * @param value int值 + * @return 16进制字符串 + * @since 4.4.1 + */ + public static String toHex(long value) { + return Long.toHexString(value); + } + + /** + * 将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) { + final char[] toDigits = toLowerCase ? DIGITS_LOWER : DIGITS_UPPER; + + int high = (b & 0xf0) >>> 4;//高位 + int low = b & 0x0f;//低位 + builder.append(toDigits[high]); + builder.append(toDigits[low]); + } + + // ---------------------------------------------------------------------------------------- Private method start + /** + * 将字节数组转换为十六进制字符串 + * + * @param data byte[] + * @param toDigits 用于控制输出的char[] + * @return 十六进制String + */ + private static String encodeHexStr(byte[] data, char[] toDigits) { + return new String(encodeHex(data, toDigits)); + } + + /** + * 将字节数组转换为十六进制字符数组 + * + * @param data byte[] + * @param toDigits 用于控制输出的char[] + * @return 十六进制char[] + */ + private static char[] encodeHex(byte[] data, char[] toDigits) { + 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++] = toDigits[(0xF0 & data[i]) >>> 4];// 高位 + out[j++] = toDigits[0x0F & data[i]];// 低位 + } + return out; + } + + /** + * 将十六进制字符转换成一个整数 + * + * @param ch 十六进制char + * @param index 十六进制字符在字符数组中的位置 + * @return 一个整数 + * @throws RuntimeException 当ch不是一个合法的十六进制字符时,抛出运行时异常 + */ + private static int toDigit(char ch, int index) { + int digit = Character.digit(ch, 16); + if (digit == -1) { + throw new RuntimeException("Illegal hexadecimal character " + ch + " at index " + index); + } + return digit; + } + // ---------------------------------------------------------------------------------------- Private method end +} \ No newline at end of file diff --git a/hutool-core/src/main/java/cn/hutool/core/util/IdUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/IdUtil.java new file mode 100644 index 000000000..b7bca158f --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/util/IdUtil.java @@ -0,0 +1,133 @@ +package cn.hutool.core.util; + +import cn.hutool.core.lang.ObjectId; +import cn.hutool.core.lang.Singleton; +import cn.hutool.core.lang.Snowflake; +import cn.hutool.core.lang.UUID; + +/** + * ID生成器工具类,此工具类中主要封装: + * + *
+ * 1. 唯一性ID生成器:UUID、ObjectId(MongoDB)、Snowflake
+ * 
+ * + *

+ * ID相关文章见:http://calvin1978.blogcn.com/articles/uuid.html + * + * @author looly + * @since 4.1.13 + */ +public class IdUtil { + + // ------------------------------------------------------------------- UUID + /** + * 获取随机UUID + * + * @return 随机UUID + */ + public static String randomUUID() { + return UUID.randomUUID().toString(); + } + + /** + * 简化的UUID,去掉了横线 + * + * @return 简化的UUID,去掉了横线 + */ + public static String simpleUUID() { + return UUID.randomUUID().toString(true); + } + + /** + * 获取随机UUID,使用性能更好的ThreadLocalRandom生成UUID + * + * @return 随机UUID + * @since 4.1.19 + */ + public static String fastUUID() { + return UUID.fastUUID().toString(); + } + + /** + * 简化的UUID,去掉了横线,使用性能更好的ThreadLocalRandom生成UUID + * + * @return 简化的UUID,去掉了横线 + * @since 4.1.19 + */ + public static String fastSimpleUUID() { + return UUID.fastUUID().toString(true); + } + + /** + * 创建MongoDB ID生成策略实现
+ * ObjectId由以下几部分组成: + * + *

+	 * 1. Time 时间戳。
+	 * 2. Machine 所在主机的唯一标识符,一般是机器主机名的散列值。
+	 * 3. PID 进程ID。确保同一机器中不冲突
+	 * 4. INC 自增计数器。确保同一秒内产生objectId的唯一性。
+	 * 
+ * + * 参考:http://blog.csdn.net/qxc1281/article/details/54021882 + * + * @return ObjectId + */ + public static String objectId() { + return ObjectId.next(); + } + + /** + * 创建Twitter的Snowflake 算法生成器
+ * 分布式系统中,有一些需要使用全局唯一ID的场景,有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。 + * + *

+ * snowflake的结构如下(每部分用-分开):
+ * + *

+	 * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
+	 * 
+ * + * 第一位为未使用,接下来的41位为毫秒级时间(41位的长度可以使用69年)
+ * 然后是5位datacenterId和5位workerId(10位的长度最多支持部署1024个节点)
+ * 最后12位是毫秒内的计数(12位的计数顺序号支持每个节点每毫秒产生4096个ID序号) + * + *

+ * 参考:http://www.cnblogs.com/relucent/p/4955340.html + * + * @param workerId 终端ID + * @param datacenterId 数据中心ID + * @return {@link Snowflake} + */ + public static Snowflake createSnowflake(long workerId, long datacenterId) { + return new Snowflake(workerId, datacenterId); + } + + /** + * 获取单例的Twitter的Snowflake 算法生成器对象
+ * 分布式系统中,有一些需要使用全局唯一ID的场景,有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。 + * + *

+ * snowflake的结构如下(每部分用-分开):
+ * + *

+	 * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
+	 * 
+ * + * 第一位为未使用,接下来的41位为毫秒级时间(41位的长度可以使用69年)
+ * 然后是5位datacenterId和5位workerId(10位的长度最多支持部署1024个节点)
+ * 最后12位是毫秒内的计数(12位的计数顺序号支持每个节点每毫秒产生4096个ID序号) + * + *

+ * 参考:http://www.cnblogs.com/relucent/p/4955340.html + * + * @param workerId 终端ID + * @param datacenterId 数据中心ID + * @return {@link Snowflake} + * @since 4.5.9 + */ + public static Snowflake getSnowflake(long workerId, long datacenterId) { + return Singleton.get(Snowflake.class, workerId, datacenterId); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/util/IdcardUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/IdcardUtil.java new file mode 100644 index 000000000..a1376ffd0 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/util/IdcardUtil.java @@ -0,0 +1,592 @@ +package cn.hutool.core.util; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +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.Validator; + +/** + * 身份证相关工具类
+ * 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 Map cityCodes = new HashMap(); + /** 台湾身份首字母对应数字 */ + private static Map twFirstCode = new HashMap(); + /** 香港身份首字母对应数字 */ + private static Map hkFirstCode = new HashMap(); + + static { + cityCodes.put("11", "北京"); + cityCodes.put("12", "天津"); + cityCodes.put("13", "河北"); + cityCodes.put("14", "山西"); + cityCodes.put("15", "内蒙古"); + cityCodes.put("21", "辽宁"); + cityCodes.put("22", "吉林"); + cityCodes.put("23", "黑龙江"); + cityCodes.put("31", "上海"); + cityCodes.put("32", "江苏"); + cityCodes.put("33", "浙江"); + cityCodes.put("34", "安徽"); + cityCodes.put("35", "福建"); + cityCodes.put("36", "江西"); + cityCodes.put("37", "山东"); + cityCodes.put("41", "河南"); + cityCodes.put("42", "湖北"); + cityCodes.put("43", "湖南"); + cityCodes.put("44", "广东"); + cityCodes.put("45", "广西"); + cityCodes.put("46", "海南"); + cityCodes.put("50", "重庆"); + cityCodes.put("51", "四川"); + cityCodes.put("52", "贵州"); + cityCodes.put("53", "云南"); + cityCodes.put("54", "西藏"); + cityCodes.put("61", "陕西"); + cityCodes.put("62", "甘肃"); + cityCodes.put("63", "青海"); + cityCodes.put("64", "宁夏"); + cityCodes.put("65", "新疆"); + cityCodes.put("71", "台湾"); + cityCodes.put("81", "香港"); + cityCodes.put("82", "澳门"); + cityCodes.put("91", "国外"); + + twFirstCode.put("A", 10); + twFirstCode.put("B", 11); + twFirstCode.put("C", 12); + twFirstCode.put("D", 13); + twFirstCode.put("E", 14); + twFirstCode.put("F", 15); + twFirstCode.put("G", 16); + twFirstCode.put("H", 17); + twFirstCode.put("J", 18); + twFirstCode.put("K", 19); + twFirstCode.put("L", 20); + twFirstCode.put("M", 21); + twFirstCode.put("N", 22); + twFirstCode.put("P", 23); + twFirstCode.put("Q", 24); + twFirstCode.put("R", 25); + twFirstCode.put("S", 26); + twFirstCode.put("T", 27); + twFirstCode.put("U", 28); + twFirstCode.put("V", 29); + twFirstCode.put("X", 30); + twFirstCode.put("Y", 31); + twFirstCode.put("W", 32); + twFirstCode.put("Z", 33); + twFirstCode.put("I", 34); + twFirstCode.put("O", 35); + + //来自http://shenfenzheng.bajiu.cn/?rid=40 + hkFirstCode.put("A", 1);// 持证人拥有香港居留权 + hkFirstCode.put("B", 2);// 持证人所报称的出生日期或地点自首次登记以后,曾作出更改 + hkFirstCode.put("C", 3);// 持证人登记领证时在香港的居留受到入境事务处处长的限制 + hkFirstCode.put("N", 14);// 持证人所报的姓名自首次登记以后,曾作出更改 + hkFirstCode.put("O", 15);// 持证人报称在香港、澳门及中国以外其他地区或国家出生 + hkFirstCode.put("R", 18);// 持证人拥有香港入境权 + hkFirstCode.put("U", 21);// 持证人登记领证时在香港的居留不受入境事务处处长的限制 + hkFirstCode.put("W", 23);// 持证人报称在澳门地区出生 + hkFirstCode.put("X", 24);// 持证人报称在中国大陆出生 + hkFirstCode.put("Z", 26);// 持证人报称在香港出生 + } + + /** + * 将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 (Validator.isNumber(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.substring(0, 6)).append(sYear).append(idCard.substring(8)); + // 获取校验位 + char sVal = getCheckCode18(idCard18.toString()); + idCard18.append(sVal); + } else { + return null; + } + return idCard18.toString(); + } + + /** + * 是否有效身份证号 + * + * @param idCard 身份证号,支持18位、15位和港澳台的10位 + * @return 是否有效 + */ + public static boolean isValidCard(String idCard) { + 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); + if (null != cardval && cardval[2].equals("true")) { + return true; + } else { + return false; + } + } + 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. + *
+ * + * @param idCard 待验证的身份证 + * @return 是否有效的18位身份证 + */ + public static boolean isvalidCard18(String idCard) { + if (CHINA_ID_MAX_LENGTH != idCard.length()) { + return false; + } + + //校验生日 + if(false == Validator.isBirthday(idCard.substring(6, 14))) { + return false; + } + + // 前17位 + String code17 = idCard.substring(0, 17); + // 第18位 + char code18 = Character.toLowerCase(idCard.charAt(17)); + if (Validator.isNumber(code17)) { + // 获取校验位 + char val = getCheckCode18(code17); + if (val == code18) { + return true; + } + } + return false; + } + + /** + * 验证15位身份编码是否合法 + * + * @param idCard 身份编码 + * @return 是否合法 + */ + public static boolean isvalidCard15(String idCard) { + if (CHINA_ID_MIN_LENGTH != idCard.length()) { + return false; + } + if (Validator.isNumber(idCard)) { + // 省份 + String proCode = idCard.substring(0, 2); + if (null == cityCodes.get(proCode)) { + return false; + } + + //校验生日(两位年份,补充为19XX) + if(false == Validator.isBirthday("19" + idCard.substring(6, 12))) { + return false; + } + } else { + return false; + } + return true; + } + + /** + * 验证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] = "台湾"; + String char2 = idCard.substring(1, 2); + if (char2.equals("1")) { + info[1] = "M"; + } else if (char2.equals("2")) { + info[1] = "F"; + } else { + info[1] = "N"; + info[2] = "false"; + return info; + } + info[2] = isValidTWCard(idCard) ? "true" : "false"; + } else if (idCard.matches("^[1|5|7][0-9]{6}\\(?[0-9A-Z]\\)?$")) { // 澳门 + info[0] = "澳门"; + info[1] = "N"; + } 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(StrUtil.isEmpty(idCard)) { + return false; + } + String start = idCard.substring(0, 1); + String mid = idCard.substring(1, 9); + String end = idCard.substring(9, 10); + Integer iStart = twFirstCode.get(start); + if(null == iStart) { + return false; + } + int sum = iStart / 10 + (iStart % 10) * 9; + final char[] chars = mid.toCharArray(); + Integer iflag = 8; + for (char c : chars) { + sum += Integer.valueOf(String.valueOf(c)) * iflag; + iflag--; + } + return (sum % 10 == 0 ? 0 : (10 - sum % 10)) == Integer.valueOf(end) ? true : false; + } + + /** + * 验证香港身份证号码(存在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("[\\(|\\)]", ""); + Integer sum = 0; + if (card.length() == 9) { + sum = (Integer.valueOf(card.substring(0, 1).toUpperCase().toCharArray()[0]) - 55) * 9 + (Integer.valueOf(card.substring(1, 2).toUpperCase().toCharArray()[0]) - 55) * 8; + card = card.substring(1, 9); + } else { + sum = 522 + (Integer.valueOf(card.substring(0, 1).toUpperCase().toCharArray()[0]) - 55) * 8; + } + String mid = card.substring(1, 7); + String end = card.substring(7, 8); + char[] chars = mid.toCharArray(); + Integer iflag = 7; + for (char c : chars) { + sum = sum + Integer.valueOf(String.valueOf(c)) * iflag; + iflag--; + } + if ("A".equals(end.toUpperCase())) { + sum += 10; + } else { + sum += Integer.valueOf(end); + } + return (sum % 11 == 0) ? true : false; + } + + /** + * 根据身份编号获取生日,只支持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) { + final Integer len = idCard.length(); + if (len < CHINA_ID_MIN_LENGTH) { + return null; + } else if (len == CHINA_ID_MIN_LENGTH) { + idCard = convert15To18(idCard); + } + return 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) { + Integer len = idCard.length(); + if (len < CHINA_ID_MIN_LENGTH) { + return null; + } else if (len == CHINA_ID_MIN_LENGTH) { + idCard = convert15To18(idCard); + } + return Short.valueOf(idCard.substring(6, 10)); + } + + /** + * 根据身份编号获取生日月,只支持15或18位身份证号码 + * + * @param idCard 身份编号 + * @return 生日(MM) + */ + public static Short getMonthByIdCard(String idCard) { + Integer len = idCard.length(); + if (len < CHINA_ID_MIN_LENGTH) { + return null; + } else if (len == CHINA_ID_MIN_LENGTH) { + idCard = convert15To18(idCard); + } + return Short.valueOf(idCard.substring(10, 12)); + } + + /** + * 根据身份编号获取生日天,只支持15或18位身份证号码 + * + * @param idCard 身份编号 + * @return 生日(dd) + */ + public static Short getDayByIdCard(String idCard) { + Integer len = idCard.length(); + if (len < CHINA_ID_MIN_LENGTH) { + return null; + } else if (len == CHINA_ID_MIN_LENGTH) { + idCard = convert15To18(idCard); + } + return Short.valueOf(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 = idCard.charAt(16); + int gender = -1; + if (Integer.parseInt(String.valueOf(sCardChar)) % 2 != 0) { + gender = 1; + } else { + gender = 0; + } + return gender; + } + + /** + * 根据身份编号获取户籍省份,只支持15或18位身份证号码 + * + * @param idCard 身份编码 + * @return 省级编码。 + */ + public static String getProvinceByIdCard(String idCard) { + int len = idCard.length(); + if (len == CHINA_ID_MIN_LENGTH || len == CHINA_ID_MAX_LENGTH) { + String sProvinNum = idCard.substring(0, 2); + return cityCodes.get(sProvinNum); + } + return null; + } + + /** + * 隐藏指定位置的几个身份证号数字为“*” + * + * @param idCard 身份证号 + * @param startInclude 开始位置(包含) + * @param endExclude 结束位置(不包含) + * @return 隐藏后的身份证号码 + * @since 3.2.2 + * @see StrUtil#hide(CharSequence, int, int) + */ + public static String hide(String idCard, int startInclude, int endExclude) { + return StrUtil.hide(idCard, startInclude, endExclude); + } + + // ----------------------------------------------------------------------------------- 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.valueOf(String.valueOf(iArr[i])) * power[i]; + } + } + return iSum; + } + // ----------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-core/src/main/java/cn/hutool/core/util/ImageUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/ImageUtil.java new file mode 100644 index 000000000..48b9c2ded --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/util/ImageUtil.java @@ -0,0 +1,17 @@ +package cn.hutool.core.util; + +import cn.hutool.core.img.ImgUtil; + +/** + * 图片处理工具类:
+ * 功能:缩放图像、切割图像、旋转、图像类型转换、彩色转黑白、文字水印、图片水印等
+ * 参考:http://blog.csdn.net/zhangzhikaixinya/article/details/8459400 + * + * @author Looly + * @deprecated 请使用{@link ImgUtil} + */ +@Deprecated +public class ImageUtil extends ImgUtil{ + + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/util/ModifierUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/ModifierUtil.java new file mode 100644 index 000000000..72cf5b262 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/util/ModifierUtil.java @@ -0,0 +1,210 @@ +package cn.hutool.core.util; + +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 static 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 int value; + + /** + * 构造 + * @param modifier 修饰符int表示,见{@link Modifier} + */ + private 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); + } + + //-------------------------------------------------------------------------------------------------------- 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/hutool-core/src/main/java/cn/hutool/core/util/NumberUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/NumberUtil.java new file mode 100644 index 000000000..084ddb622 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/util/NumberUtil.java @@ -0,0 +1,2335 @@ +package cn.hutool.core.util; + +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.Random; +import java.util.Set; + +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.lang.Assert; + +/** + * 数字工具类
+ * 对于精确值计算应该使用 {@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 DEFAUT_DIV_SCALE = 10; + + /** + * 提供精确的加法运算 + * + * @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) { + 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 = new BigDecimal(null == value ? "0" : value.toString()); + for (int i = 1; i < values.length; i++) { + value = values[i]; + if (null != value) { + result = result.add(new BigDecimal(value.toString())); + } + } + 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 = new BigDecimal(null == value ? "0" : value); + for (int i = 1; i < values.length; i++) { + value = values[i]; + if (null != value) { + result = result.add(new BigDecimal(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 = null == value ? BigDecimal.ZERO : 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) { + 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 = new BigDecimal(null == value ? "0" : value.toString()); + for (int i = 1; i < values.length; i++) { + value = values[i]; + if (null != value) { + result = result.subtract(new BigDecimal(value.toString())); + } + } + 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 = new BigDecimal(null == value ? "0" : value); + for (int i = 1; i < values.length; i++) { + value = values[i]; + if (null != value) { + result = result.subtract(new BigDecimal(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 = null == value ? BigDecimal.ZERO : 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) { + 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)) { + return BigDecimal.ZERO; + } + + Number value = values[0]; + BigDecimal result = new BigDecimal(null == value ? "0" : value.toString()); + for (int i = 1; i < values.length; i++) { + value = values[i]; + if (null != value) { + 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)) { + return BigDecimal.ZERO; + } + + String value = values[0]; + BigDecimal result = new BigDecimal(null == value ? "0" : value); + for (int i = 1; i < values.length; i++) { + value = values[i]; + if (null != value) { + result = result.multiply(new BigDecimal(value)); + } + } + return result; + } + + /** + * 提供精确的乘法运算
+ * 如果传入多个值为null或者空,则返回0 + * + * @param values 多个被乘值 + * @return 积 + * @since 4.0.0 + */ + public static BigDecimal mul(BigDecimal... values) { + if (ArrayUtil.isEmpty(values)) { + return BigDecimal.ZERO; + } + + BigDecimal value = values[0]; + BigDecimal result = null == value ? BigDecimal.ZERO : value; + for (int i = 1; i < values.length; i++) { + value = values[i]; + if (null != value) { + result = result.multiply(value); + } + } + return result; + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况的时候,精确到小数点后10位,后面的四舍五入 + * + * @param v1 被除数 + * @param v2 除数 + * @return 两个参数的商 + */ + public static double div(float v1, float v2) { + return div(v1, v2, DEFAUT_DIV_SCALE); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况的时候,精确到小数点后10位,后面的四舍五入 + * + * @param v1 被除数 + * @param v2 除数 + * @return 两个参数的商 + */ + public static double div(float v1, double v2) { + return div(v1, v2, DEFAUT_DIV_SCALE); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况的时候,精确到小数点后10位,后面的四舍五入 + * + * @param v1 被除数 + * @param v2 除数 + * @return 两个参数的商 + */ + public static double div(double v1, float v2) { + return div(v1, v2, DEFAUT_DIV_SCALE); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况的时候,精确到小数点后10位,后面的四舍五入 + * + * @param v1 被除数 + * @param v2 除数 + * @return 两个参数的商 + */ + public static double div(double v1, double v2) { + return div(v1, v2, DEFAUT_DIV_SCALE); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况的时候,精确到小数点后10位,后面的四舍五入 + * + * @param v1 被除数 + * @param v2 除数 + * @return 两个参数的商 + */ + public static double div(Double v1, Double v2) { + return div(v1, v2, DEFAUT_DIV_SCALE); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况的时候,精确到小数点后10位,后面的四舍五入 + * + * @param v1 被除数 + * @param v2 除数 + * @return 两个参数的商 + * @since 3.1.0 + */ + public static BigDecimal div(Number v1, Number v2) { + return div(v1, v2, DEFAUT_DIV_SCALE); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况的时候,精确到小数点后10位,后面的四舍五入 + * + * @param v1 被除数 + * @param v2 除数 + * @return 两个参数的商 + */ + public static BigDecimal div(String v1, String v2) { + return div(v1, v2, DEFAUT_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) { + 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) { + return div(v1.toString(), v2.toString(), 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(new BigDecimal(v1), new BigDecimal(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); + } + + // ------------------------------------------------------------------------------------------- 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).toString(); + } + + /** + * 保留固定位数小数
+ * 采用四舍五入策略 {@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).toString(); + } + + /** + * 保留固定位数小数
+ * 例如保留四位小数: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).toString(); + } + + /** + * 保留固定位数小数
+ * 例如保留四位小数: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).toString(); + } + + /** + * 四舍六入五成双计算法 + *

+ * 四舍六入五成双是一种比较精确比较科学的计数保留法,是一种数字修约规则。 + *

+ * + *
+	 * 算法规则:
+	 * 四舍六入五考虑,
+	 * 五后非零就进一,
+	 * 五后皆零看奇偶,
+	 * 五前为偶应舍去,
+	 * 五前为奇要进一。
+	 * 
+ * + * @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) { + 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); + } + + /** + * 格式化金额输出,每三位用逗号分隔 + * + * @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 + /** + * 是否为数字 + * + * @param str 字符串值 + * @return 是否为数字 + */ + public static boolean isNumber(String str) { + if (StrUtil.isBlank(str)) { + return false; + } + char[] chars = str.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] == '-') ? 1 : 0; + if (sz > start + 1) { + if (chars[start] == '0' && 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是否是整数
+ * 支持8、10、16进制 + * + * @param s String + * @return 是否为整数 + */ + public static boolean isInteger(String s) { + try { + Integer.parseInt(s); + } catch (NumberFormatException e) { + return false; + } + return true; + } + + /** + * 判断字符串是否是Long类型
+ * 支持8、10、16进制 + * + * @param s String + * @return 是否为{@link Long}类型 + * @since 4.0.0 + */ + public static boolean isLong(String s) { + try { + Long.parseLong(s); + } catch (NumberFormatException e) { + return false; + } + return true; + } + + /** + * 判断字符串是否是浮点数 + * + * @param s String + * @return 是否为{@link Double}类型 + */ + public static boolean isDouble(String s) { + try { + Double.parseDouble(s); + if (s.contains(".")) { + return true; + } + return false; + } catch (NumberFormatException e) { + return false; + } + } + + /** + * 是否是质数(素数)
+ * 质数表的质数又称素数。指整数在一个大于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) { + if (begin > end) { + int temp = begin; + begin = end; + end = temp; + } + // 加入逻辑判断,确保begin end) { + int temp = begin; + begin = end; + end = temp; + } + // 加入逻辑判断,确保begin set = new HashSet(); + while (set.size() < size) { + set.add(begin + ran.nextInt(end - begin)); + } + + Integer[] ranArr = set.toArray(new Integer[size]); + return ranArr; + } + + // ------------------------------------------------------------------------------------------- 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) * ... * end + *

+ * + * @param start 阶乘起始 + * @param end 阶乘结束 + * @return 结果 + * @since 4.1.0 + */ + public static long factorial(long start, long end) { + if (start < end) { + return 0L; + } + if (start == end) { + return 1L; + } + return start * factorial(start - 1, end); + } + + /** + * 计算阶乘 + *

+ * n! = n * (n-1) * ... * 2 * 1 + *

+ * + * @param n 阶乘起始 + * @return 结果 + */ + public static long factorial(long n) { + return factorial(n, 1); + } + + /** + * 平方根算法
+ * 推荐使用 {@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 + + /** + * 比较两个值的大小 + * + * @see Character#compare(char, char) + * + * @param x 第一个值 + * @param y 第二个值 + * @return x==y返回0,x<y返回-1,x>y返回1 + * @since 3.0.1 + */ + public static int compare(char x, char y) { + return x - y; + } + + /** + * 比较两个值的大小 + * + * @see Double#compare(double, double) + * + * @param x 第一个值 + * @param y 第二个值 + * @return x==y返回0,x<y返回-1,x>y返回1 + * @since 3.0.1 + */ + public static int compare(double x, double y) { + return Double.compare(x, y); + } + + /** + * 比较两个值的大小 + * + * @see Integer#compare(int, int) + * + * @param x 第一个值 + * @param y 第二个值 + * @return x==y返回0,x<y返回-1,x>y返回1 + * @since 3.0.1 + */ + public static int compare(int x, int y) { + if (x == y) { + return 0; + } + if (x < y) { + return -1; + } else { + return 1; + } + } + + /** + * 比较两个值的大小 + * + * @see Long#compare(long, long) + * + * @param x 第一个值 + * @param y 第二个值 + * @return x==y返回0,x<y返回-1,x>y返回1 + * @since 3.0.1 + */ + public static int compare(long x, long y) { + if (x == y) { + return 0; + } + if (x < y) { + return -1; + } else { + return 1; + } + } + + /** + * 比较两个值的大小 + * + * @see Short#compare(short, short) + * + * @param x 第一个值 + * @param y 第二个值 + * @return x==y返回0,x<y返回-1,x>y返回1 + * @since 3.0.1 + */ + public static int compare(short x, short y) { + if (x == y) { + return 0; + } + if (x < y) { + return -1; + } else { + return 1; + } + } + + /** + * 比较两个值的大小 + * + * @see Byte#compare(byte, byte) + * + * @param x 第一个值 + * @param y 第二个值 + * @return x==y返回0,x<y返回-1,x>y返回1 + * @since 3.0.1 + */ + public static int compare(byte x, byte y) { + return 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; + } + + /** + * 比较大小,值相等 返回true
+ * 此方法通过调用{@link BigDecimal#compareTo(BigDecimal)}方法来判断是否相等
+ * 此方法判断值相等时忽略精度的,既0.00 == 0 + * + * @param bigNum1 数字1 + * @param bigNum2 数字2 + * @return 是否相等 + */ + public static boolean equals(BigDecimal bigNum1, BigDecimal bigNum2) { + Assert.notNull(bigNum1); + Assert.notNull(bigNum2); + return 0 == bigNum1.compareTo(bigNum2); + } + + /** + * 比较两个字符是否相同 + * + * @param c1 字符1 + * @param c2 字符2 + * @param ignoreCase 是否忽略大小写 + * @return 是否相同 + * @since 3.2.1 + * @see CharUtil#equals(char, char, boolean) + */ + public static boolean equals(char c1, char c2, boolean ignoreCase) { + return CharUtil.equals(c1, c2, ignoreCase); + } + + /** + * 取最小值 + * + * @param 元素类型 + * @param numberArray 数字数组 + * @return 最小值 + * @since 4.0.7 + * @see ArrayUtil#min(Comparable[]) + */ + @SuppressWarnings("unchecked") + public static > T min(T... numberArray) { + return ArrayUtil.min(numberArray); + } + + /** + * 取最小值 + * + * @param numberArray 数字数组 + * @return 最小值 + * @since 4.0.7 + * @see ArrayUtil#min(long...) + */ + public static long min(long... numberArray) { + return ArrayUtil.min(numberArray); + } + + /** + * 取最小值 + * + * @param numberArray 数字数组 + * @return 最小值 + * @since 4.0.7 + * @see ArrayUtil#min(int...) + */ + public static int min(int... numberArray) { + return ArrayUtil.min(numberArray); + } + + /** + * 取最小值 + * + * @param numberArray 数字数组 + * @return 最小值 + * @since 4.0.7 + * @see ArrayUtil#min(short...) + */ + public static short min(short... numberArray) { + return ArrayUtil.min(numberArray); + } + + /** + * 取最小值 + * + * @param numberArray 数字数组 + * @return 最小值 + * @since 4.0.7 + * @see ArrayUtil#min(double...) + */ + public static double min(double... numberArray) { + return ArrayUtil.min(numberArray); + } + + /** + * 取最小值 + * + * @param numberArray 数字数组 + * @return 最小值 + * @since 4.0.7 + * @see ArrayUtil#min(float...) + */ + public static float min(float... numberArray) { + return ArrayUtil.min(numberArray); + } + + /** + * 取最大值 + * + * @param 元素类型 + * @param numberArray 数字数组 + * @return 最大值 + * @since 4.0.7 + * @see ArrayUtil#max(Comparable[]) + */ + @SuppressWarnings("unchecked") + public static > T max(T... numberArray) { + return ArrayUtil.max(numberArray); + } + + /** + * 取最大值 + * + * @param numberArray 数字数组 + * @return 最大值 + * @since 4.0.7 + * @see ArrayUtil#max(long...) + */ + public static long max(long... numberArray) { + return ArrayUtil.max(numberArray); + } + + /** + * 取最大值 + * + * @param numberArray 数字数组 + * @return 最大值 + * @since 4.0.7 + * @see ArrayUtil#max(int...) + */ + public static int max(int... numberArray) { + return ArrayUtil.max(numberArray); + } + + /** + * 取最大值 + * + * @param numberArray 数字数组 + * @return 最大值 + * @since 4.0.7 + * @see ArrayUtil#max(short...) + */ + public static short max(short... numberArray) { + return ArrayUtil.max(numberArray); + } + + /** + * 取最大值 + * + * @param numberArray 数字数组 + * @return 最大值 + * @since 4.0.7 + * @see ArrayUtil#max(double...) + */ + public static double max(double... numberArray) { + return ArrayUtil.max(numberArray); + } + + /** + * 取最大值 + * + * @param numberArray 数字数组 + * @return 最大值 + * @since 4.0.7 + * @see ArrayUtil#max(float...) + */ + public static float max(float... 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()},并去除尾小数点儿后多余的0 + * + * @param number A Number + * @return A String. + */ + public static String toStr(Number number) { + if (null == number) { + throw new NullPointerException("Number is null !"); + } + + if (false == ObjectUtil.isValidIfNumber(number)) { + throw new IllegalArgumentException("Number is non-finite!"); + } + + // 去掉小数点儿后多余的0 + String string = number.toString(); + 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} + * + * @param number 数字 + * @return {@link BigDecimal} + * @since 4.0.9 + */ + public static BigDecimal toBigDecimal(Number number) { + if (null == number) { + return BigDecimal.ZERO; + } + return toBigDecimal(number.toString()); + } + + /** + * 数字转{@link BigDecimal} + * + * @param number 数字 + * @return {@link BigDecimal} + * @since 4.0.9 + */ + public static BigDecimal toBigDecimal(String number) { + return (null == number) ? BigDecimal.ZERO : new BigDecimal(number); + } + + /** + * 是否空白符
+ * 空白符包括空格、制表符、全角空格和不间断空格
+ * + * @param c 字符 + * @return 是否空白符 + * @see Character#isWhitespace(int) + * @see Character#isSpaceChar(int) + * @since 3.0.6 + * @deprecated 请使用{@link CharUtil#isBlankChar(char)} + */ + @Deprecated + public static boolean isBlankChar(char c) { + return isBlankChar((int) c); + } + + /** + * 是否空白符
+ * 空白符包括空格、制表符、全角空格和不间断空格
+ * + * @see Character#isWhitespace(int) + * @see Character#isSpaceChar(int) + * @param c 字符 + * @return 是否空白符 + * @since 3.0.6 + * @deprecated 请使用{@link CharUtil#isBlankChar(int)} + */ + @Deprecated + public static boolean isBlankChar(int c) { + return Character.isWhitespace(c) || Character.isSpaceChar(c) || c == '\ufeff' || c == '\u202a'; + } + + /** + * 计算等份个数 + * + * @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 = 0; + if (total % partCount == 0) { + partValue = total / partCount; + } else { + partValue = (int) Math.floor(total / partCount); + if (isPlusOneWhenHasRem) { + partValue += 1; + } + } + 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); + } + + /** + * 解析转换数字字符串为int型数字,规则如下: + * + *
+	 * 1、0x开头的视为16进制数字
+	 * 2、0开头的视为8进制数字
+	 * 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; + } + + // 对于带小数转换为整数采取去掉小数的策略 + number = StrUtil.subBefore(number, CharUtil.DOT, false); + if (StrUtil.isEmpty(number)) { + return 0; + } + + if (StrUtil.startWithIgnoreCase(number, "0x")) { + // 0x04表示16进制数 + return Integer.parseInt(number.substring(2), 16); + } + + return Integer.parseInt(removeNumberFlag(number)); + } + + /** + * 解析转换数字字符串为long型数字,规则如下: + * + *
+	 * 1、0x开头的视为16进制数字
+	 * 2、0开头的视为8进制数字
+	 * 3、空串返回0
+	 * 4、其它情况按照10进制转换
+	 * 
+ * + * @param number 数字,支持0x开头、0开头和普通十进制 + * @return long + * @since 4.1.4 + */ + public static long parseLong(String number) { + if (StrUtil.isBlank(number)) { + return 0; + } + + // 对于带小数转换为整数采取去掉小数的策略 + number = StrUtil.subBefore(number, CharUtil.DOT, false); + if (StrUtil.isEmpty(number)) { + return 0; + } + + if (number.startsWith("0x")) { + // 0x04表示16进制数 + return Long.parseLong(number.substring(2), 16); + } + + return Long.parseLong(removeNumberFlag(number)); + } + + /** + * 将指定字符串转换为{@link Number} 对象 + * + * @param numberStr Number字符串 + * @return Number对象 + * @since 4.1.15 + */ + public static Number parseNumber(String numberStr) { + numberStr = removeNumberFlag(numberStr); + try { + return NumberFormat.getInstance().parse(numberStr); + } catch (ParseException e) { + throw new UtilException(e); + } + } + + /** + * 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 + * @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); + } + + // ------------------------------------------------------------------------------------------- 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); + } + } + + /** + * 去掉数字尾部的数字标识,例如12D,44.0F,22L中的最后一个字母 + * + * @param number 数字字符串 + * @return 去掉标识的字符串 + */ + private static String removeNumberFlag(String number) { + // 去掉类型标识的结尾 + final int lastPos = number.length() - 1; + final char lastCharUpper = Character.toUpperCase(number.charAt(lastPos)); + if ('D' == lastCharUpper || 'L' == lastCharUpper || 'F' == lastCharUpper) { + number = StrUtil.subPre(number, lastPos); + } + return number; + } + // ------------------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-core/src/main/java/cn/hutool/core/util/ObjectUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/ObjectUtil.java new file mode 100644 index 000000000..42ba4c59c --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/util/ObjectUtil.java @@ -0,0 +1,536 @@ +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.io.FastByteArrayOutputStream; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.map.MapUtil; + +import java.io.ByteArrayInputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.Map; + +/** + * 对象工具类,包括判空、克隆、序列化等操作 + * + * @author Looly + */ +public class ObjectUtil { + + /** + * 比较两个对象是否相等。
+ * 相同的条件有两个,满足其一即可:
+ *
    + *
  1. obj1 == null && obj2 == null
  2. + *
  3. obj1.equals(obj2)
  4. + *
+ * 1. obj1 == null && obj2 == null 2. obj1.equals(obj2) + * + * @param obj1 对象1 + * @param obj2 对象2 + * @return 是否相等 + */ + public static boolean equal(Object obj1, Object obj2) { + // return (obj1 != null) ? (obj1.equals(obj2)) : (obj2 == null); + return (obj1 == obj2) || (obj1 != null && obj1.equals(obj2)); + } + + /** + * 比较两个对象是否不相等。
+ * + * @param obj1 对象1 + * @param obj2 对象2 + * @return 是否不等 + * @since 3.0.7 + */ + public static boolean notEqual(Object obj1, Object obj2) { + return false == 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) { + Iterator iter = (Iterator) obj; + count = 0; + while (iter.hasNext()) { + count++; + iter.next(); + } + return count; + } + if (obj instanceof Enumeration) { + Enumeration enumeration = (Enumeration) obj; + count = 0; + while (enumeration.hasMoreElements()) { + count++; + enumeration.nextElement(); + } + return count; + } + if (obj.getClass().isArray() == true) { + 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).values().contains(element); + } + + if (obj instanceof Iterator) { + Iterator iter = (Iterator) obj; + while (iter.hasNext()) { + Object o = iter.next(); + if (equal(o, element)) { + return true; + } + } + return false; + } + if (obj instanceof Enumeration) { + Enumeration enumeration = (Enumeration) obj; + while (enumeration.hasMoreElements()) { + Object o = enumeration.nextElement(); + if (equal(o, element)) { + return true; + } + } + return false; + } + if (obj.getClass().isArray() == true) { + int len = Array.getLength(obj); + for (int i = 0; i < len; i++) { + 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) { + return null == obj || obj.equals(null); + } + + /** + * 检查对象是否不为null + * + * @param obj 对象 + * @return 是否为null + */ + public static boolean isNotNull(Object obj) { + return null != obj && false == 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 false == 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 (null != object) ? object : defaultValue; + } + + /** + * 克隆对象
+ * 如果对象实现Cloneable接口,调用其clone方法
+ * 如果实现Serializable接口,执行深度克隆
+ * 否则返回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封装 + */ + @SuppressWarnings("unchecked") + public static T cloneByStream(T obj) { + if (null == obj || false == (obj instanceof Serializable)) { + return null; + } + final FastByteArrayOutputStream byteOut = new FastByteArrayOutputStream(); + ObjectOutputStream out = null; + try { + out = new ObjectOutputStream(byteOut); + out.writeObject(obj); + out.flush(); + final ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(byteOut.toByteArray())); + return (T) in.readObject(); + } catch (Exception e) { + throw new UtilException(e); + } finally { + IoUtil.close(out); + } + } + + /** + * 序列化
+ * 对象必须实现Serializable接口 + * + * @param 对象类型 + * @param obj 要被序列化的对象 + * @return 序列化后的字节码 + */ + public static byte[] serialize(T obj) { + if (null == obj || false == (obj instanceof Serializable)) { + return null; + } + + FastByteArrayOutputStream byteOut = new FastByteArrayOutputStream(); + ObjectOutputStream oos = null; + try { + oos = new ObjectOutputStream(byteOut); + oos.writeObject(obj); + oos.flush(); + } catch (Exception e) { + throw new UtilException(e); + } finally { + IoUtil.close(oos); + } + return byteOut.toByteArray(); + } + + /** + * 反序列化
+ * 对象必须实现Serializable接口 + * + * @param 对象类型 + * @param bytes 反序列化的字节码 + * @return 反序列化后的对象 + */ + @SuppressWarnings("unchecked") + public static T unserialize(byte[] bytes) { + ObjectInputStream ois = null; + try { + ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + ois = new ObjectInputStream(bais); + return (T) ois.readObject(); + } catch (Exception e) { + throw new UtilException(e); + } + } + + /** + * 是否为基本类型,包括包装类型和非包装类型 + * + * @param object 被检查对象 + * @return 是否为基本类型 + * @see ClassUtil#isBasicType(Class) + */ + public static boolean isBasicType(Object object) { + 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 != null && obj instanceof Number) { + if (obj instanceof Double) { + if (((Double) obj).isInfinite() || ((Double) obj).isNaN()) { + return false; + } + } else if (obj instanceof Float) { + if (((Float) obj).isInfinite() || ((Float) obj).isNaN()) { + return false; + } + } + } + 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 java.util.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 java.util.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 + * + * @param obj Bean对象 + * @return Bean所有字段转为Map后的字符串 + * @since 3.2.0 + */ + public static String toString(Object obj) { + if (null == obj) { + return "null"; + } + if (obj instanceof Map) { + return ((Map) 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#isEmpty(Object)} 判断元素 + * + * @param objs 被检查对象 + * @return 是否存在 + */ + 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/hutool-core/src/main/java/cn/hutool/core/util/PageUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/PageUtil.java new file mode 100644 index 000000000..7177a7b17 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/util/PageUtil.java @@ -0,0 +1,135 @@ +package cn.hutool.core.util; + +/** + * 分页工具类 + * + * @author xiaoleilu + * + */ +public class PageUtil { + + /** + * 将页数和每页条目数转换为开始位置
+ * 此方法用于不包括结束位置的分页方法
+ * 例如: + * + *
+	 * 页码:1,每页10 =》 0
+	 * 页码:2,每页10 =》 10
+	 * ……
+	 * 
+ * + * @param pageNo 页码(从1计数) + * @param pageSize 每页条目数 + * @return 开始位置 + */ + public static int getStart(int pageNo, int pageSize) { + if (pageNo < 1) { + pageNo = 1; + } + + if (pageSize < 1) { + pageSize = 0; + } + + return (pageNo - 1) * pageSize; + } + + /** + * 将页数和每页条目数转换为开始位置和结束位置
+ * 此方法用于不包括结束位置的分页方法
+ * 例如: + * + *
+	 * 页码:1,每页10 =》 [0, 10]
+	 * 页码:2,每页10 =》 [10, 20]
+	 * ……
+	 * 
+ * + * @param pageNo 页码(从1计数) + * @param pageSize 每页条目数 + * @return 第一个数为开始位置,第二个数为结束位置 + */ + public static int[] transToStartEnd(int pageNo, int pageSize) { + final int start = getStart(pageNo, pageSize); + if (pageSize < 1) { + pageSize = 0; + } + final int end = start + pageSize; + + return new int[] { start, end }; + } + + /** + * 根据总数计算总页数 + * + * @param totalCount 总数 + * @param pageSize 每页数 + * @return 总页数 + */ + public static int totalPage(int totalCount, int pageSize) { + if (pageSize == 0) { + return 0; + } + return totalCount % pageSize == 0 ? (totalCount / pageSize) : (totalCount / pageSize + 1); + } + + /** + * 分页彩虹算法
+ * 来自:https://github.com/iceroot/iceroot/blob/master/src/main/java/com/icexxx/util/IceUtil.java
+ * 通过传入的信息,生成一个分页列表显示 + * + * @param currentPage 当前页 + * @param pageCount 总页数 + * @param displayCount 每屏展示的页数 + * @return 分页条 + */ + public static int[] rainbow(int currentPage, int pageCount, int displayCount) { + boolean isEven = true; + isEven = displayCount % 2 == 0; + int left = displayCount / 2; + int right = displayCount / 2; + + int length = displayCount; + if (isEven) { + right++; + } + if (pageCount < displayCount) { + length = pageCount; + } + int[] result = new int[length]; + if (pageCount >= displayCount) { + if (currentPage <= left) { + for (int i = 0; i < result.length; i++) { + result[i] = i + 1; + } + } else if (currentPage > pageCount - right) { + for (int i = 0; i < result.length; i++) { + result[i] = i + pageCount - displayCount + 1; + } + } else { + for (int i = 0; i < result.length; i++) { + result[i] = i + currentPage - 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); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/util/PinyinUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/PinyinUtil.java new file mode 100644 index 000000000..585a7c5be --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/util/PinyinUtil.java @@ -0,0 +1,204 @@ +package cn.hutool.core.util; + +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.text.StrBuilder; + +/** + * 拼音工具类
+ * 注意:部分拼音并不准确,例如:怡 + * + * @author looly + * @since 4.0.7 + * @deprecated 此工具不再建议使用,因为某些汉字拼音不准确且无法处理多音字,建议使用Jpinyin或Pinyin4j + */ +@Deprecated +public class PinyinUtil { + + /** 汉字对应ascii范围 */ + private static int[] pinyinValue = new int[] { -20319, -20317, -20304, -20295, -20292, -20283, -20265, -20257, -20242, -20230, -20051, -20036, -20032, -20026, -20002, -19990, -19986, -19982, + -19976, -19805, -19784, -19775, -19774, -19763, -19756, -19751, -19746, -19741, -19739, -19728, -19725, -19715, -19540, -19531, -19525, -19515, -19500, -19484, -19479, -19467, -19289, + -19288, -19281, -19275, -19270, -19263, -19261, -19249, -19243, -19242, -19238, -19235, -19227, -19224, -19218, -19212, -19038, -19023, -19018, -19006, -19003, -18996, -18977, -18961, + -18952, -18783, -18774, -18773, -18763, -18756, -18741, -18735, -18731, -18722, -18710, -18697, -18696, -18526, -18518, -18501, -18490, -18478, -18463, -18448, -18447, -18446, -18239, + -18237, -18231, -18220, -18211, -18201, -18184, -18183, -18181, -18012, -17997, -17988, -17970, -17964, -17961, -17950, -17947, -17931, -17928, -17922, -17759, -17752, -17733, -17730, + -17721, -17703, -17701, -17697, -17692, -17683, -17676, -17496, -17487, -17482, -17468, -17454, -17433, -17427, -17417, -17202, -17185, -16983, -16970, -16942, -16915, -16733, -16708, + -16706, -16689, -16664, -16657, -16647, -16474, -16470, -16465, -16459, -16452, -16448, -16433, -16429, -16427, -16423, -16419, -16412, -16407, -16403, -16401, -16393, -16220, -16216, + -16212, -16205, -16202, -16187, -16180, -16171, -16169, -16158, -16155, -15959, -15958, -15944, -15933, -15920, -15915, -15903, -15889, -15878, -15707, -15701, -15681, -15667, -15661, + -15659, -15652, -15640, -15631, -15625, -15454, -15448, -15436, -15435, -15419, -15416, -15408, -15394, -15385, -15377, -15375, -15369, -15363, -15362, -15183, -15180, -15165, -15158, + -15153, -15150, -15149, -15144, -15143, -15141, -15140, -15139, -15128, -15121, -15119, -15117, -15110, -15109, -14941, -14937, -14933, -14930, -14929, -14928, -14926, -14922, -14921, + -14914, -14908, -14902, -14894, -14889, -14882, -14873, -14871, -14857, -14678, -14674, -14670, -14668, -14663, -14654, -14645, -14630, -14594, -14429, -14407, -14399, -14384, -14379, + -14368, -14355, -14353, -14345, -14170, -14159, -14151, -14149, -14145, -14140, -14137, -14135, -14125, -14123, -14122, -14112, -14109, -14099, -14097, -14094, -14092, -14090, -14087, + -14083, -13917, -13914, -13910, -13907, -13906, -13905, -13896, -13894, -13878, -13870, -13859, -13847, -13831, -13658, -13611, -13601, -13406, -13404, -13400, -13398, -13395, -13391, + -13387, -13383, -13367, -13359, -13356, -13343, -13340, -13329, -13326, -13318, -13147, -13138, -13120, -13107, -13096, -13095, -13091, -13076, -13068, -13063, -13060, -12888, -12875, + -12871, -12860, -12858, -12852, -12849, -12838, -12831, -12829, -12812, -12802, -12607, -12597, -12594, -12585, -12556, -12359, -12346, -12320, -12300, -12120, -12099, -12089, -12074, + -12067, -12058, -12039, -11867, -11861, -11847, -11831, -11798, -11781, -11604, -11589, -11536, -11358, -11340, -11339, -11324, -11303, -11097, -11077, -11067, -11055, -11052, -11045, + -11041, -11038, -11024, -11020, -11019, -11018, -11014, -10838, -10832, -10815, -10800, -10790, -10780, -10764, -10587, -10544, -10533, -10519, -10331, -10329, -10328, -10322, -10315, + -10309, -10307, -10296, -10281, -10274, -10270, -10262, -10260, -10256, -10254 }; + + private static String[] pinyinStr = new String[] { "a", "ai", "an", "ang", "ao", "ba", "bai", "ban", "bang", "bao", "bei", "ben", "beng", "bi", "bian", "biao", "bie", "bin", "bing", "bo", "bu", + "ca", "cai", "can", "cang", "cao", "ce", "ceng", "cha", "chai", "chan", "chang", "chao", "che", "chen", "cheng", "chi", "chong", "chou", "chu", "chuai", "chuan", "chuang", "chui", "chun", + "chuo", "ci", "cong", "cou", "cu", "cuan", "cui", "cun", "cuo", "da", "dai", "dan", "dang", "dao", "de", "deng", "di", "dian", "diao", "die", "ding", "diu", "dong", "dou", "du", "duan", + "dui", "dun", "duo", "e", "en", "er", "fa", "fan", "fang", "fei", "fen", "feng", "fo", "fou", "fu", "ga", "gai", "gan", "gang", "gao", "ge", "gei", "gen", "geng", "gong", "gou", "gu", + "gua", "guai", "guan", "guang", "gui", "gun", "guo", "ha", "hai", "han", "hang", "hao", "he", "hei", "hen", "heng", "hong", "hou", "hu", "hua", "huai", "huan", "huang", "hui", "hun", + "huo", "ji", "jia", "jian", "jiang", "jiao", "jie", "jin", "jing", "jiong", "jiu", "ju", "juan", "jue", "jun", "ka", "kai", "kan", "kang", "kao", "ke", "ken", "keng", "kong", "kou", "ku", + "kua", "kuai", "kuan", "kuang", "kui", "kun", "kuo", "la", "lai", "lan", "lang", "lao", "le", "lei", "leng", "li", "lia", "lian", "liang", "liao", "lie", "lin", "ling", "liu", "long", + "lou", "lu", "lv", "luan", "lue", "lun", "luo", "ma", "mai", "man", "mang", "mao", "me", "mei", "men", "meng", "mi", "mian", "miao", "mie", "min", "ming", "miu", "mo", "mou", "mu", "na", + "nai", "nan", "nang", "nao", "ne", "nei", "nen", "neng", "ni", "nian", "niang", "niao", "nie", "nin", "ning", "niu", "nong", "nu", "nv", "nuan", "nue", "nuo", "o", "ou", "pa", "pai", + "pan", "pang", "pao", "pei", "pen", "peng", "pi", "pian", "piao", "pie", "pin", "ping", "po", "pu", "qi", "qia", "qian", "qiang", "qiao", "qie", "qin", "qing", "qiong", "qiu", "qu", + "quan", "que", "qun", "ran", "rang", "rao", "re", "ren", "reng", "ri", "rong", "rou", "ru", "ruan", "rui", "run", "ruo", "sa", "sai", "san", "sang", "sao", "se", "sen", "seng", "sha", + "shai", "shan", "shang", "shao", "she", "shen", "sheng", "shi", "shou", "shu", "shua", "shuai", "shuan", "shuang", "shui", "shun", "shuo", "si", "song", "sou", "su", "suan", "sui", "sun", + "suo", "ta", "tai", "tan", "tang", "tao", "te", "teng", "ti", "tian", "tiao", "tie", "ting", "tong", "tou", "tu", "tuan", "tui", "tun", "tuo", "wa", "wai", "wan", "wang", "wei", "wen", + "weng", "wo", "wu", "xi", "xia", "xian", "xiang", "xiao", "xie", "xin", "xing", "xiong", "xiu", "xu", "xuan", "xue", "xun", "ya", "yan", "yang", "yao", "ye", "yi", "yin", "ying", "yo", + "yong", "you", "yu", "yuan", "yue", "yun", "za", "zai", "zan", "zang", "zao", "ze", "zei", "zen", "zeng", "zha", "zhai", "zhan", "zhang", "zhao", "zhe", "zhen", "zheng", "zhi", "zhong", + "zhou", "zhu", "zhua", "zhuai", "zhuan", "zhuang", "zhui", "zhun", "zhuo", "zi", "zong", "zou", "zu", "zuan", "zui", "zun", "zuo" }; + + /** + * 获取所给中文的每个汉字首字母组成首字母字符串 + * + * @param chinese 汉字字符串 + * @return 首字母字符串 + */ + public static String getAllFirstLetter(String chinese) { + if (StrUtil.isBlank(chinese)) { + return StrUtil.EMPTY; + } + + int len = chinese.length(); + final StrBuilder strBuilder = new StrBuilder(len); + for (int i = 0; i < len; i++) { + strBuilder.append(getFirstLetter(chinese.charAt(i))); + } + + return strBuilder.toString(); + } + + /** + * 获取拼音首字母
+ * 传入汉字,返回拼音首字母
+ * 如果传入为字母,返回其小写形式
+ * 感谢【帝都】宁静 提供方法 + * + * @param ch 汉字 + * @return 首字母,小写 + */ + public static char getFirstLetter(char ch) { + if (ch >= 'a' && ch <= 'z') { + return ch; + } + if (ch >= 'A' && ch <= 'Z') { + return Character.toLowerCase(ch); + } + final byte[] bys = String.valueOf(ch).getBytes(CharsetUtil.CHARSET_GBK); + if (bys.length == 1) { + return ch; + } + int count = (bys[0] + 256) * 256 + bys[1] + 256; + if (count < 45217) { + return ch; + } else if (count < 45253) { + return 'a'; + } else if (count < 45761) { + return 'b'; + } else if (count < 46318) { + return 'c'; + } else if (count < 46826) { + return 'd'; + } else if (count < 47010) { + return 'e'; + } else if (count < 47297) { + return 'f'; + } else if (count < 47614) { + return 'g'; + } else if (count < 48119) { + return 'h'; + } else if (count < 49062) { + return 'j'; + } else if (count < 49324) { + return 'k'; + } else if (count < 49896) { + return 'l'; + } else if (count < 50371) { + return 'm'; + } else if (count < 50614) { + return 'n'; + } else if (count < 50622) { + return 'o'; + } else if (count < 50906) { + return 'p'; + } else if (count < 51387) { + return 'q'; + } else if (count < 51446) { + return 'r'; + } else if (count < 52218) { + return 's'; + } else if (count < 52698) { + return 't'; + } else if (count < 52980) { + return 'w'; + } else if (count < 53689) { + return 'x'; + } else if (count < 54481) { + return 'y'; + } else if (count < 55290) { + return 'z'; + } + return ch; + } + + /** + * 汉字转拼音 + *
+ * example : 张三 zhangsan + * + * @param chinese 汉字 + * @return 对应的拼音 + * @since 4.0.11 + */ + public static String getPinYin(String chinese) { + final StrBuilder result = StrUtil.strBuilder(); + String strTemp = null; + int len = chinese.length(); + for (int j = 0; j < len; j++) { + strTemp = chinese.substring(j, j + 1); + int ascii = getChsAscii(strTemp); + if (ascii > 0) { + //非汉字 + result.append((char)ascii); + } else { + for (int i = pinyinValue.length - 1; i >= 0; i--) { + if (pinyinValue[i] <= ascii) { + result.append(pinyinStr[i]); + break; + } + } + } + } + return result.toString(); + } + + //------------------------------------------------------------------------------------------------------- Private method start + /** + * 获取汉字对应的ascii码 + * @param chs 汉字 + * @return ascii码 + */ + private static int getChsAscii(String chs) { + int asc = 0; + byte[] bytes = chs.getBytes(CharsetUtil.CHARSET_GBK); + switch (bytes.length) { + case 1: + // 英文字符 + asc = bytes[0]; + break; + case 2: + // 中文字符 + int hightByte = 256 + bytes[0]; + int lowByte = 256 + bytes[1]; + asc = (256 * hightByte + lowByte) - 256 * 256; + break; + default: + throw new UtilException("Illegal resource string"); + } + return asc; + } + //------------------------------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-core/src/main/java/cn/hutool/core/util/RandomUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/RandomUtil.java new file mode 100644 index 000000000..e6c3f6fc2 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/util/RandomUtil.java @@ -0,0 +1,524 @@ +package cn.hutool.core.util; + +import java.awt.Color; +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.HashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; + +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.UUID; +import cn.hutool.core.lang.WeightRandom; +import cn.hutool.core.lang.WeightRandom.WeightObj; + +/** + * 随机工具类 + * + * @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之后提供并发产生随机数,能够解决多个线程发生的竞争争夺。 + * + * @return {@link ThreadLocalRandom} + * @since 3.1.2 + */ + public static ThreadLocalRandom getRandom() { + return ThreadLocalRandom.current(); + } + + /** + * 获取{@link SecureRandom},类提供加密的强随机数生成器 (RNG) + * + * @return {@link SecureRandom} + * @since 3.1.2 + */ + public static SecureRandom getSecureRandom() { + try { + return SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e) { + throw new UtilException(e); + } + } + + /** + * 获取随机数产生器 + * + * @param isSecure 是否为强随机数生成器 (RNG) + * @return {@link Random} + * @since 4.1.15 + * @see #getSecureRandom() + * @see #getRandom() + */ + 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); + } + + /** + * 获得指定范围内的随机数 + * + * @param min 最小数(包含) + * @param max 最大数(不包含) + * @return 随机数 + */ + public static int randomInt(int min, int max) { + return getRandom().nextInt(min, max); + } + + /** + * 获得随机数[0, 2^32) + * + * @return 随机数 + */ + public static int randomInt() { + return getRandom().nextInt(); + } + + /** + * 获得指定范围内的随机数 [0,limit) + * + * @param limit 限制随机数的范围,不包括这个数 + * @return 随机数 + */ + public static int randomInt(int limit) { + return getRandom().nextInt(limit); + } + + /** + * 获得指定范围内的随机数[min, max) + * + * @param min 最小数(包含) + * @param max 最大数(不包含) + * @return 随机数 + * @since 3.3.0 + */ + public static long randomLong(long min, long max) { + return getRandom().nextLong(min, max); + } + + /** + * 获得随机数 + * + * @return 随机数 + * @since 3.3.0 + */ + public static long randomLong() { + return getRandom().nextLong(); + } + + /** + * 获得指定范围内的随机数 [0,limit) + * + * @param limit 限制随机数的范围,不包括这个数 + * @return 随机数 + */ + public static long randomLong(long limit) { + return getRandom().nextLong(limit); + } + + /** + * 获得指定范围内的随机数 + * + * @param min 最小数(包含) + * @param max 最大数(不包含) + * @return 随机数 + * @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 随机数 + * @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 随机数 + * @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) { + 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) { + 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; + } + + /** + * 随机获得列表中的一定量的不重复元素,返回Set + * + * @param 元素类型 + * @param collection 列表 + * @param count 随机取出的个数 + * @return 随机元素 + * @throws IllegalArgumentException 需要的长度大于给定集合非重复总数 + */ + public static Set randomEleSet(Collection collection, int count) { + ArrayList source = new ArrayList<>(new HashSet<>(collection)); + if (count > source.size()) { + throw new IllegalArgumentException("Count is larger than collection distinct size !"); + } + + final HashSet result = new HashSet(count); + int limit = collection.size(); + while (result.size() < count) { + result.add(randomEle(source, limit)); + } + + return result; + } + + /** + * 获得一个随机的字符串(只包含数字和字符) + * + * @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 字符串的长度 + * @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) { + final StringBuilder sb = new StringBuilder(); + + if (length < 1) { + length = 1; + } + int baseLength = baseString.length(); + for (int i = 0; i < length; i++) { + int number = getRandom().nextInt(baseLength); + sb.append(baseString.charAt(number)); + } + return sb.toString(); + } + + /** + * 随机数字,数字为0~9单个数字 + * + * @return 随机数字字符 + * @since 3.1.2 + */ + public static int 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(getRandom().nextInt(baseString.length())); + } + + /** + * 生成随机颜色 + * + * @return 随机颜色 + * @since 4.1.5 + */ + public static Color randomColor() { + final Random random = getRandom(); + return new Color(random.nextInt(255), random.nextInt(255), random.nextInt(255)); + } + + /** + * 带有权重的随机生成器 + * + * @param weightObjs 带有权重的对象列表 + * @return {@link WeightRandom} + * @since 4.0.3 + */ + public static WeightRandom weightRandom(WeightObj[] weightObjs) { + return new WeightRandom<>(weightObjs); + } + + /** + * 带有权重的随机生成器 + * + * @param weightObjs 带有权重的对象列表 + * @return {@link WeightRandom} + * @since 4.0.3 + */ + public static WeightRandom weightRandom(Iterable> weightObjs) { + return new WeightRandom<>(weightObjs); + } + + // ------------------------------------------------------------------- UUID + /** + * @return 随机UUID + * @deprecated 请使用{@link IdUtil#randomUUID()} + */ + @Deprecated + public static String randomUUID() { + return UUID.randomUUID().toString(); + } + + /** + * 简化的UUID,去掉了横线 + * + * @return 简化的UUID,去掉了横线 + * @since 3.2.2 + * @deprecated 请使用{@link IdUtil#simpleUUID()} + */ + @Deprecated + public static String simpleUUID() { + return UUID.randomUUID().toString(true); + } + + /** + * 以当天为基准,随机产生一个日期 + * + * @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/hutool-core/src/main/java/cn/hutool/core/util/ReUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/ReUtil.java new file mode 100644 index 000000000..ae7e72349 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/util/ReUtil.java @@ -0,0 +1,727 @@ +package cn.hutool.core.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.lang.Holder; +import cn.hutool.core.lang.PatternPool; +import cn.hutool.core.lang.Validator; +import cn.hutool.core.lang.func.Func1; + +/** + * 正则相关工具类
+ * 常用正则请见 {@link Validator} + * + * @author xiaoleilu + */ +public class ReUtil { + + /** 正则表达式匹配中文汉字 */ + public final static String RE_CHINESE = "[\u4E00-\u9FFF]"; + /** 正则表达式匹配中文字符串 */ + public final static String RE_CHINESES = RE_CHINESE + "+"; + + /** 正则中需要被转义的关键字 */ + public final static Set RE_KEYS = CollectionUtil.newHashSet(new Character[] { '$', '(', ')', '*', '+', '.', '[', ']', '?', '\\', '^', '{', '}', '|' }); + + /** + * 获得匹配的字符串,获得正则中分组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; + } + + // Pattern pattern = Pattern.compile(regex, Pattern.DOTALL); + final Pattern pattern = PatternPool.get(regex, Pattern.DOTALL); + return get(pattern, content, groupIndex); + } + + /** + * 获得匹配的字符串,,获得正则中分组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 Matcher matcher = pattern.matcher(content); + if (matcher.find()) { + return matcher.group(groupIndex); + } + return null; + } + + /** + * 获得匹配的字符串匹配到的所有分组 + * + * @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) { + if (null == content || null == pattern) { + return null; + } + + ArrayList result = new ArrayList<>(); + final Matcher matcher = pattern.matcher(content); + if (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)); + } + } + 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<>(new Comparator() { + @Override + public int compare(Integer o1, Integer o2) { + return 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; + } + + // Pattern pattern = Pattern.compile(regex, Pattern.DOTALL); + 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 新字符串 + */ + public static String extractMultiAndDelPre(Pattern pattern, Holder 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, Holder contentHolder, String template) { + if (null == contentHolder || null == regex || null == template) { + return null; + } + + // Pattern pattern = Pattern.compile(regex, Pattern.DOTALL); + 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); + } + + // Pattern pattern = Pattern.compile(regex, Pattern.DOTALL); + 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) { + if (null == pattern || StrUtil.isBlank(content)) { + return StrUtil.str(content); + } + + return pattern.matcher(content).replaceFirst(StrUtil.EMPTY); + } + + /** + * 删除匹配的全部内容 + * + * @param regex 正则 + * @param content 被匹配的内容 + * @return 删除后剩余的内容 + */ + public static String delAll(String regex, CharSequence content) { + if (StrUtil.hasBlank(regex, content)) { + return StrUtil.str(content); + } + + // Pattern pattern = Pattern.compile(regex, Pattern.DOTALL); + 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.isBlank(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); + } + + // Pattern pattern = Pattern.compile(regex, Pattern.DOTALL); + final Pattern pattern = PatternPool.get(regex, Pattern.DOTALL); + 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(Pattern.compile(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; + } + + if (null == collection) { + throw new NullPointerException("Null collection param provided!"); + } + + final Matcher matcher = pattern.matcher(content); + while (matcher.find()) { + collection.add(matcher.group(group)); + } + return collection; + } + + /** + * 计算指定字符串中,匹配pattern的个数 + * + * @param regex 正则表达式 + * @param content 被查找的内容 + * @return 匹配个数 + */ + public static int count(String regex, CharSequence content) { + if (null == regex || null == content) { + return 0; + } + + // Pattern pattern = Pattern.compile(regex, Pattern.DOTALL); + 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 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 HashSet()); + final StringBuffer sb = new StringBuffer(); + do { + String replacement = replacementTemplate; + for (String var : varNums) { + 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); + } + + /** + * 替换所有正则匹配的文本,并使用自定义函数决定如何替换 + * + * @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); + } + + /** + * 替换所有正则匹配的文本,并使用自定义函数决定如何替换 + * + * @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(); + } +} \ No newline at end of file diff --git a/hutool-core/src/main/java/cn/hutool/core/util/ReferenceUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/ReferenceUtil.java new file mode 100644 index 000000000..cb21b1994 --- /dev/null +++ b/hutool-core/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); + case WEAK: + return new WeakReference<>(referent); + case PHANTOM: + return new PhantomReference(referent, queue); + default: + return null; + } + } + + /** + * 引用类型 + * + * @author looly + * + */ + public static enum ReferenceType { + /** 软引用,在GC报告内存不足时会被GC回收 */ + SOFT, + /** 弱引用,在GC时发现弱引用会回收其对象 */ + WEAK, + /** + * 虚引用,在GC时发现虚引用对象,会将{@link PhantomReference}插入{@link ReferenceQueue}。
+ * 此时对象未被真正回收,要等到{@link ReferenceQueue}被真正处理后才会被回收。 + */ + PHANTOM; + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/util/ReflectUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/ReflectUtil.java new file mode 100644 index 000000000..5317059b8 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/util/ReflectUtil.java @@ -0,0 +1,813 @@ +package cn.hutool.core.util; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.Filter; +import cn.hutool.core.lang.SimpleCache; + +/** + * 反射工具类 + * + * @author Looly + * @since 3.0.9 + */ +public class ReflectUtil { + + /** 构造对象缓存 */ + private static final SimpleCache, Constructor[]> CONSTRUCTORS_CACHE = new SimpleCache<>(); + /** 字段缓存 */ + private static final SimpleCache, Field[]> FIELDS_CACHE = new SimpleCache<>(); + /** 方法缓存 */ + private static final SimpleCache, Method[]> METHODS_CACHE = new SimpleCache<>(); + + // --------------------------------------------------------------------------------------------------------- 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)) { + // 构造可访问 + constructor.setAccessible(true); + return (Constructor) constructor; + } + } + return null; + } + + /** + * 获得一个类中所有构造列表 + * + * @param 构造的对象类型 + * @param beanClass 类 + * @return 字段列表 + * @throws SecurityException 安全检查异常 + */ + @SuppressWarnings("unchecked") + public static Constructor[] getConstructors(Class beanClass) throws SecurityException { + Assert.notNull(beanClass); + Constructor[] constructors = CONSTRUCTORS_CACHE.get(beanClass); + if (null != constructors) { + return (Constructor[]) constructors; + } + + constructors = getConstructorsDirectly(beanClass); + return (Constructor[]) CONSTRUCTORS_CACHE.put(beanClass, constructors); + } + + /** + * 获得一个类中所有字段列表,直接反射获取,无缓存 + * + * @param beanClass 类 + * @return 字段列表 + * @throws SecurityException 安全检查异常 + */ + public static Constructor[] getConstructorsDirectly(Class beanClass) throws SecurityException { + Assert.notNull(beanClass); + 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); + } + + /** + * 查找指定类中的所有字段(包括非public字段),也包括父类和Object类的字段, 字段不存在则返回null + * + * @param beanClass 被查找字段的类,不能为null + * @param name 字段名 + * @return 字段 + * @throws SecurityException 安全异常 + */ + public static Field getField(Class beanClass, String name) throws SecurityException { + final Field[] fields = getFields(beanClass); + if (ArrayUtil.isNotEmpty(fields)) { + for (Field field : fields) { + if ((name.equals(field.getName()))) { + return field; + } + } + } + return null; + } + + /** + * 获得一个类中所有字段列表,包括其父类中的字段 + * + * @param beanClass 类 + * @return 字段列表 + * @throws SecurityException 安全检查异常 + */ + public static Field[] getFields(Class beanClass) throws SecurityException { + Field[] allFields = FIELDS_CACHE.get(beanClass); + if (null != allFields) { + return allFields; + } + + allFields = getFieldsDirectly(beanClass, true); + return FIELDS_CACHE.put(beanClass, allFields); + } + + /** + * 获得一个类中所有字段列表,直接反射获取,无缓存 + * + * @param beanClass 类 + * @param withSuperClassFieds 是否包括父类的字段列表 + * @return 字段列表 + * @throws SecurityException 安全检查异常 + */ + public static Field[] getFieldsDirectly(Class beanClass, boolean withSuperClassFieds) 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 = withSuperClassFieds ? searchType.getSuperclass() : null; + } + + return allFields; + } + + /** + * 获取字段值 + * + * @param obj 对象 + * @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.getClass(), fieldName)); + } + + /** + * 获取字段值 + * + * @param obj 对象 + * @param field 字段 + * @return 字段值 + * @throws UtilException 包装IllegalAccessException异常 + */ + public static Object getFieldValue(Object obj, Field field) throws UtilException { + if (null == obj || null == field) { + return null; + } + field.setAccessible(true); + Object result = null; + try { + result = field.get(obj); + } catch (IllegalAccessException e) { + throw new UtilException(e, "IllegalAccess for {}.{}", obj.getClass(), field.getName()); + } + return result; + } + + /** + * 获取所有字段的值 + * @param obj bean对象 + * @return 字段值数组 + * @since 4.1.17 + */ + public static Object[] getFieldsValue(Object obj) { + if (null != obj) { + final Field[] fields = getFields(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; + } + + /** + * 设置字段值 + * + * @param obj 对象 + * @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); + setFieldValue(obj, getField(obj.getClass(), fieldName), value); + } + + /** + * 设置字段值 + * + * @param obj 对象 + * @param field 字段 + * @param value 值,值类型必须与字段类型匹配,不会自动转换对象类型 + * @throws UtilException UtilException 包装IllegalAccessException异常 + */ + public static void setFieldValue(Object obj, Field field, Object value) throws UtilException { + Assert.notNull(obj); + Assert.notNull(field); + field.setAccessible(true); + + if(null != value) { + Class fieldType = field.getType(); + if(false == fieldType.isAssignableFrom(value.getClass())) { + //对于类型不同的字段,尝试转换,转换失败则使用原对象类型 + final Object targetValue = Convert.convert(fieldType, value); + if(null != targetValue) { + value = targetValue; + } + } + } + + try { + field.set(obj, value); + } catch (IllegalAccessException e) { + throw new UtilException(e, "IllegalAccess for {}.{}", obj.getClass(), 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方法列表 + * + * @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 = CollectionUtil.newArrayList(methods); + } + return methodList; + } + + /** + * 获得指定类过滤后的Public方法列表 + * + * @param clazz 查找方法的类 + * @param excludeMethods 不包括的方法 + * @return 过滤后的方法列表 + */ + public static List getPublicMethods(Class clazz, Method... excludeMethods) { + final HashSet excludeMethodSet = CollectionUtil.newHashSet(excludeMethods); + return getPublicMethods(clazz, new Filter() { + @Override + public boolean accept(Method method) { + return false == excludeMethodSet.contains(method); + } + }); + } + + /** + * 获得指定类过滤后的Public方法列表 + * + * @param clazz 查找方法的类 + * @param excludeMethodNames 不包括的方法名列表 + * @return 过滤后的方法列表 + */ + public static List getPublicMethods(Class clazz, String... excludeMethodNames) { + final HashSet excludeMethodNameSet = CollectionUtil.newHashSet(excludeMethodNames); + return getPublicMethods(clazz, new Filter() { + @Override + public boolean accept(Method method) { + return false == excludeMethodNameSet.contains(method.getName()); + } + }); + } + + /** + * 查找指定Public方法 如果找不到对应的方法或方法不为public的则返回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类的方法 + * + *

+ * 此方法为精准获取方法名,即方法名和参数数量和类型必须一致,否则返回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)); + } + + /** + * 忽略大小写查找指定方法,如果找不到对应的方法则返回null + * + *

+ * 此方法为精准获取方法名,即方法名和参数数量和类型必须一致,否则返回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); + } + + /** + * 查找指定方法 如果找不到对应的方法则返回null + * + *

+ * 此方法为精准获取方法名,即方法名和参数数量和类型必须一致,否则返回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); + } + + /** + * 查找指定方法 如果找不到对应的方法则返回null + * + *

+ * 此方法为精准获取方法名,即方法名和参数数量和类型必须一致,否则返回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; + } + + final Method[] methods = getMethods(clazz); + if (ArrayUtil.isNotEmpty(methods)) { + for (Method method : methods) { + if (StrUtil.equals(methodName, method.getName(), ignoreCase)) { + if (ClassUtil.isAllAssignableFrom(method.getParameterTypes(), paramTypes)) { + return method; + } + } + } + } + return null; + } + + /** + * 按照方法名查找指定方法名的方法,只返回匹配到的第一个方法,如果找不到对应的方法则返回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); + } + + /** + * 按照方法名查找指定方法名的方法,只返回匹配到的第一个方法,如果找不到对应的方法则返回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); + } + + /** + * 按照方法名查找指定方法名的方法,只返回匹配到的第一个方法,如果找不到对应的方法则返回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; + } + + final Method[] methods = getMethods(clazz); + if (ArrayUtil.isNotEmpty(methods)) { + for (Method method : methods) { + if (StrUtil.equals(methodName, method.getName(), ignoreCase)) { + return method; + } + } + } + return null; + } + + /** + * 获得指定类中的Public方法名
+ * 去重重载的方法 + * + * @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; + } + + /** + * 获得指定类过滤后的Public方法列表 + * + * @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 类 + * @return 方法列表 + * @throws SecurityException 安全检查异常 + */ + public static Method[] getMethods(Class beanClass) throws SecurityException { + Method[] allMethods = METHODS_CACHE.get(beanClass); + if (null != allMethods) { + return allMethods; + } + + allMethods = getMethodsDirectly(beanClass, true); + return METHODS_CACHE.put(beanClass, allMethods); + } + + /** + * 获得一个类中所有方法列表,直接反射获取,无缓存 + * + * @param beanClass 类 + * @param withSuperClassMethods 是否包括父类的方法列表 + * @return 方法列表 + * @throws SecurityException 安全检查异常 + */ + public static Method[] getMethodsDirectly(Class beanClass, boolean withSuperClassMethods) throws SecurityException { + Assert.notNull(beanClass); + + Method[] allMethods = null; + Class searchType = beanClass; + Method[] declaredMethods; + while (searchType != null) { + declaredMethods = searchType.getDeclaredMethods(); + if (null == allMethods) { + allMethods = declaredMethods; + } else { + allMethods = ArrayUtil.append(allMethods, declaredMethods); + } + searchType = withSuperClassMethods ? searchType.getSuperclass() : null; + } + + return allMethods; + } + + /** + * 是否为equals方法 + * + * @param method 方法 + * @return 是否为equals方法 + */ + public static boolean isEqualsMethod(Method method) { + if (method == null || false == method.getName().equals("equals")) { + return false; + } + final Class[] paramTypes = method.getParameterTypes(); + return (1 == paramTypes.length && paramTypes[0] == Object.class); + } + + /** + * 是否为hashCode方法 + * + * @param method 方法 + * @return 是否为hashCode方法 + */ + public static boolean isHashCodeMethod(Method method) { + return (method != null && method.getName().equals("hashCode") && method.getParameterTypes().length == 0); + } + + /** + * 是否为toString方法 + * + * @param method 方法 + * @return 是否为toString方法 + */ + public static boolean isToStringMethod(Method method) { + return (method != null && method.getName().equals("toString") && method.getParameterTypes().length == 0); + } + + // --------------------------------------------------------------------------------------------------------- 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); + 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); + } + } + + /** + * 尝试遍历并调用此类的所有构造方法,直到构造成功并返回 + * + * @param 对象类型 + * @param beanClass 被构造的类 + * @return 构造后的对象 + */ + public static T newInstanceIfPossible(Class beanClass) { + Assert.notNull(beanClass); + try { + return newInstance(beanClass); + } catch (Exception e) { + // ignore + // 默认构造不存在的情况下查找其它构造 + } + + final Constructor[] constructors = getConstructors(beanClass); + Class[] parameterTypes; + for (Constructor constructor : constructors) { + parameterTypes = constructor.getParameterTypes(); + if (0 == parameterTypes.length) { + continue; + } + constructor.setAccessible(true); + try { + return constructor.newInstance(ClassUtil.getDefaultValues(parameterTypes)); + } catch (Exception e) { + // 构造出错时继续尝试下一种构造方式 + continue; + } + } + 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 对象,如果执行静态方法,此值为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 != types && 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); + } + + /** + * 执行方法 + * + * @param 返回对象类型 + * @param obj 对象,如果执行静态方法,此值为null + * @param method 方法(对象方法或static方法都可) + * @param args 参数对象 + * @return 结果 + * @throws UtilException 一些列异常的包装 + */ + @SuppressWarnings("unchecked") + public static T invoke(Object obj, Method method, Object... args) throws UtilException { + if (false == method.isAccessible()) { + method.setAccessible(true); + } + + try { + return (T) method.invoke(ClassUtil.isStatic(method) ? null : obj, args); + } catch (Exception e) { + throw new UtilException(e); + } + } + + /** + * 执行对象中指定方法 + * + * @param 返回对象类型 + * @param obj 方法所在对象 + * @param methodName 方法名 + * @param args 参数列表 + * @return 执行结果 + * @throws UtilException IllegalAccessException包装 + * @since 3.1.2 + */ + public static T invoke(Object obj, String methodName, Object... args) throws UtilException { + final Method method = getMethodOfObj(obj, methodName, args); + if (null == method) { + throw new UtilException(StrUtil.format("No such method: [{}]", methodName)); + } + return invoke(obj, method, args); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/util/RuntimeUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/RuntimeUtil.java new file mode 100644 index 000000000..5c687c464 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/util/RuntimeUtil.java @@ -0,0 +1,253 @@ +package cn.hutool.core.util; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; + +/** + * 系统运行时工具类,用于执行系统命令的工具 + * + * @author Looly + * @since 3.1.1 + */ +public class RuntimeUtil { + + /** + * 执行系统命令,使用系统默认编码 + * + * @param cmds 命令列表,每个元素代表一条命令 + * @return 执行结果 + * @throws IORuntimeException IO异常 + */ + public static String execForStr(String... cmds) throws IORuntimeException { + return execForStr(CharsetUtil.systemCharset(), cmds); + } + + /** + * 执行系统命令,使用系统默认编码 + * + * @param charset 编码 + * @param cmds 命令列表,每个元素代表一条命令 + * @return 执行结果 + * @throws IORuntimeException IO异常 + * @since 3.1.2 + */ + public static String execForStr(Charset charset, String... cmds) throws IORuntimeException { + return getResult(exec(cmds), charset); + } + + /** + * 执行系统命令,使用系统默认编码 + * + * @param cmds 命令列表,每个元素代表一条命令 + * @return 执行结果,按行区分 + * @throws IORuntimeException IO异常 + */ + public static List execForLines(String... cmds) throws IORuntimeException { + return execForLines(CharsetUtil.systemCharset(), cmds); + } + + /** + * 执行系统命令,使用系统默认编码 + * + * @param charset 编码 + * @param cmds 命令列表,每个元素代表一条命令 + * @return 执行结果,按行区分 + * @throws IORuntimeException IO异常 + * @since 3.1.2 + */ + public static List execForLines(Charset charset, String... cmds) throws IORuntimeException { + return getResultLines(exec(cmds), charset); + } + + /** + * 执行命令
+ * 命令带参数时参数可作为其中一个参数,也可以将命令和参数组合为一个字符串传入 + * + * @param cmds 命令 + * @return {@link Process} + */ + public static Process exec(String... cmds) { + if (ArrayUtil.isEmpty(cmds)) { + throw new NullPointerException("Command is empty !"); + } + + // 单条命令的情况 + if (1 == cmds.length) { + final String cmd = cmds[0]; + if (StrUtil.isBlank(cmd)) { + throw new NullPointerException("Command is empty !"); + } + cmds = StrUtil.splitToArray(cmd, StrUtil.C_SPACE); + } + + Process process; + try { + process = new ProcessBuilder(cmds).redirectErrorStream(true).start(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return process; + } + + /** + * 执行命令
+ * 命令带参数时参数可作为其中一个参数,也可以将命令和参数组合为一个字符串传入 + * + * @param envp 环境变量参数,传入形式为key=value,null表示继承系统环境变量 + * @param cmds 命令 + * @return {@link Process} + * @since 4.1.6 + */ + public static Process exec(String[] envp, String... cmds) { + return exec(envp, cmds); + } + + /** + * 执行命令
+ * 命令带参数时参数可作为其中一个参数,也可以将命令和参数组合为一个字符串传入 + * + * @param envp 环境变量参数,传入形式为key=value,null表示继承系统环境变量 + * @param dir 执行命令所在目录(用于相对路径命令执行),null表示使用当前进程执行的目录 + * @param cmds 命令 + * @return {@link Process} + * @since 4.1.6 + */ + public static Process exec(String[] envp, File dir, String... cmds) { + if (ArrayUtil.isEmpty(cmds)) { + throw new NullPointerException("Command is empty !"); + } + + // 单条命令的情况 + if (1 == cmds.length) { + final String cmd = cmds[0]; + if (StrUtil.isBlank(cmd)) { + throw new NullPointerException("Command is empty !"); + } + cmds = StrUtil.splitToArray(cmd, StrUtil.C_SPACE); + } + try { + return Runtime.getRuntime().exec(cmds, envp, dir); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + // -------------------------------------------------------------------------------------------------- result + /** + * 获取命令执行结果,使用系统默认编码,获取后销毁进程 + * + * @param process {@link Process} 进程 + * @return 命令执行结果列表 + */ + public static List getResultLines(Process process) { + return getResultLines(process, CharsetUtil.systemCharset()); + } + + /** + * 获取命令执行结果,使用系统默认编码,获取后销毁进程 + * + * @param process {@link Process} 进程 + * @param charset 编码 + * @return 命令执行结果列表 + * @since 3.1.2 + */ + public static List getResultLines(Process process, Charset charset) { + InputStream in = null; + try { + in = process.getInputStream(); + return IoUtil.readLines(in, charset, new ArrayList()); + } finally { + IoUtil.close(in); + destroy(process); + } + } + + /** + * 获取命令执行结果,使用系统默认编码,,获取后销毁进程 + * + * @param process {@link Process} 进程 + * @return 命令执行结果列表 + * @since 3.1.2 + */ + public static String getResult(Process process) { + return getResult(process, CharsetUtil.systemCharset()); + } + + /** + * 获取命令执行结果,获取后销毁进程 + * + * @param process {@link Process} 进程 + * @param charset 编码 + * @return 命令执行结果列表 + * @since 3.1.2 + */ + public static String getResult(Process process, Charset charset) { + InputStream in = null; + try { + in = process.getInputStream(); + return IoUtil.read(in, charset); + } finally { + IoUtil.close(in); + destroy(process); + } + } + + /** + * 获取命令执行异常结果,使用系统默认编码,,获取后销毁进程 + * + * @param process {@link Process} 进程 + * @return 命令执行结果列表 + * @since 4.1.21 + */ + public static String getErrorResult(Process process) { + return getErrorResult(process, CharsetUtil.systemCharset()); + } + + /** + * 获取命令执行异常结果,获取后销毁进程 + * + * @param process {@link Process} 进程 + * @param charset 编码 + * @return 命令执行结果列表 + * @since 4.1.21 + */ + public static String getErrorResult(Process process, Charset charset) { + InputStream in = null; + try { + in = process.getErrorStream(); + return IoUtil.read(in, charset); + } finally { + IoUtil.close(in); + destroy(process); + } + } + + /** + * 销毁进程 + * + * @param process 进程 + * @since 3.1.2 + */ + public static void destroy(Process process) { + if (null != process) { + process.destroy(); + } + } + + /** + * 增加一个JVM关闭后的钩子,用于在JVM关闭时执行某些操作 + * + * @param hook 钩子 + * @since 4.0.5 + */ + public static void addShutdownHook(Runnable hook) { + Runtime.getRuntime().addShutdownHook((hook instanceof Thread) ? (Thread) hook : new Thread(hook)); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/util/StrUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/StrUtil.java new file mode 100644 index 000000000..85d7a79f6 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/util/StrUtil.java @@ -0,0 +1,4191 @@ +package cn.hutool.core.util; + +import java.io.StringReader; +import java.io.StringWriter; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.regex.Pattern; + +import cn.hutool.core.comparator.VersionComparator; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.Matcher; +import cn.hutool.core.lang.func.Func1; +import cn.hutool.core.text.StrBuilder; +import cn.hutool.core.text.StrFormatter; +import cn.hutool.core.text.StrSpliter; +import cn.hutool.core.text.TextSimilarity; + +/** + * 字符串工具类 + * + * @author xiaoleilu + * + */ +public class StrUtil { + + public static final int INDEX_NOT_FOUND = -1; + + public static final char C_SPACE = CharUtil.SPACE; + public static final char C_TAB = CharUtil.TAB; + public static final char C_DOT = CharUtil.DOT; + public static final char C_SLASH = CharUtil.SLASH; + public static final char C_BACKSLASH = CharUtil.BACKSLASH; + public static final char C_CR = CharUtil.CR; + public static final char C_LF = CharUtil.LF; + public static final char C_UNDERLINE = CharUtil.UNDERLINE; + public static final char C_COMMA = CharUtil.COMMA; + public static final char C_DELIM_START = CharUtil.DELIM_START; + public static final char C_DELIM_END = CharUtil.DELIM_END; + public static final char C_BRACKET_START = CharUtil.BRACKET_START; + public static final char C_BRACKET_END = CharUtil.BRACKET_END; + public static final char C_COLON = CharUtil.COLON; + + public static final String SPACE = " "; + public static final String TAB = " "; + public static final String DOT = "."; + public static final String DOUBLE_DOT = ".."; + public static final String SLASH = "/"; + public static final String BACKSLASH = "\\"; + public static final String EMPTY = ""; + public static final String NULL = "null"; + public static final String CR = "\r"; + public static final String LF = "\n"; + public static final String CRLF = "\r\n"; + public static final String UNDERLINE = "_"; + public static final String DASHED = "-"; + public static final String COMMA = ","; + public static final String DELIM_START = "{"; + public static final String DELIM_END = "}"; + public static final String BRACKET_START = "["; + public static final String BRACKET_END = "]"; + public static final String COLON = ":"; + + public static final String HTML_NBSP = " "; + public static final String HTML_AMP = "&"; + public static final String HTML_QUOTE = """; + public static final String HTML_APOS = "'"; + public static final String HTML_LT = "<"; + public static final String HTML_GT = ">"; + + public static final String EMPTY_JSON = "{}"; + + // ------------------------------------------------------------------------ Blank + /** + * 字符串是否为空白 空白的定义如下:
+ * 1、为null
+ * 2、为不可见字符(如空格)
+ * 3、""
+ * + * @param str 被检测的字符串 + * @return 是否为空 + */ + public static boolean isBlank(CharSequence str) { + int length; + + if ((str == null) || ((length = str.length()) == 0)) { + return true; + } + + for (int i = 0; i < length; i++) { + // 只要有一个非空字符即为非空字符串 + if (false == CharUtil.isBlankChar(str.charAt(i))) { + return false; + } + } + + return true; + } + + /** + * 如果对象是字符串是否为空白,空白的定义如下:
+ * 1、为null
+ * 2、为不可见字符(如空格)
+ * 3、""
+ * + * @param obj 对象 + * @return 如果为字符串是否为空串 + * @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; + } + + /** + * 字符串是否为非空白 空白的定义如下:
+ * 1、不为null
+ * 2、不为不可见字符(如空格)
+ * 3、不为""
+ * + * @param str 被检测的字符串 + * @return 是否为非空 + */ + public static boolean isNotBlank(CharSequence str) { + return false == isBlank(str); + } + + /** + * 是否包含空字符串 + * + * @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; + } + + /** + * 给定所有字符串是否为空白 + * + * @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; + } + + // ------------------------------------------------------------------------ Empty + /** + * 字符串是否为空,空的定义如下:
+ * 1、为null
+ * 2、为""
+ * + * @param str 被检测的字符串 + * @return 是否为空 + */ + public static boolean isEmpty(CharSequence str) { + return str == null || str.length() == 0; + } + + /** + * 如果对象是字符串是否为空串空的定义如下:
+ * 1、为null
+ * 2、为""
+ * + * @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; + } + + /** + * 字符串是否为非空白 空白的定义如下:
+ * 1、不为null
+ * 2、不为""
+ * + * @param str 被检测的字符串 + * @return 是否为非空 + */ + public static boolean isNotEmpty(CharSequence str) { + return false == isEmpty(str); + } + + /** + * 当给定字符串为null时,转换为Empty + * + * @param str 被转换的字符串 + * @return 转换后的字符串 + */ + public static String nullToEmpty(CharSequence str) { + return nullToDefault(str, EMPTY); + } + + /** + * 如果字符串是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(); + } + + /** + * 如果字符串是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(); + } + + /** + * 如果字符串是null或者""或者空白,则返回指定默认字符串,否则返回字符串本身。 + * + *
+	 * emptyToDefault(null, "default")  = "default"
+	 * emptyToDefault("", "default")    = "default"
+	 * emptyToDefault("  ", "default")  = "default"
+	 * emptyToDefault("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(); + } + + /** + * 当给定字符串为空字符串时,转换为null + * + * @param str 被转换的字符串 + * @return 转换后的字符串 + */ + public static String emptyToNull(CharSequence str) { + return isEmpty(str) ? null : str.toString(); + } + + /** + * 是否包含空字符串 + * + * @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; + } + + /** + * 是否全部为空字符串 + * + * @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; + } + + /** + * 检查字符串是否为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 + /** + * 除去字符串头尾部的空白,如果字符串是null,依然返回null。 + * + *

+ * 注意,和String.trim不同,此方法使用NumberUtil.isBlankChar 来判定空白, 因而可以除去英文字符集之外的其它空白,如中文空格。 + * + *

+	 * trim(null)          = null
+	 * trim("")            = ""
+	 * trim("     ")       = ""
+	 * trim("abc")         = "abc"
+	 * trim("    abc    ") = "abc"
+	 * 
+ * + * @param str 要处理的字符串 + * + * @return 除去头尾空白的字符串,如果原字串为null,则返回null + */ + public static String trim(CharSequence str) { + return (null == str) ? null : trim(str, 0); + } + + /** + * 给定字符串数组全部做去首尾空格 + * + * @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] = str.trim(); + } + } + } + + /** + * 除去字符串头尾部的空白,如果字符串是{@code null},返回""。 + * + *
+	 * 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},返回""。 + * + *
+	 * 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; + } + + /** + * 除去字符串头部的空白,如果字符串是null,则返回null。 + * + *

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

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

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

+	 * trimEnd(null)       = null
+	 * trimEnd("")         = ""
+	 * trimEnd("abc")      = "abc"
+	 * trimEnd("  abc")    = "  abc"
+	 * trimEnd("abc  ")    = "abc"
+	 * trimEnd(" abc ")    = " abc"
+	 * 
+ * + * @param str 要处理的字符串 + * + * @return 除去空白的字符串,如果原字串为null或结果字符串为"",则返回 null + */ + public static String trimEnd(CharSequence str) { + return trim(str, 1); + } + + /** + * 除去字符串头尾部的空白符,如果字符串是null,依然返回null。 + * + * @param str 要处理的字符串 + * @param mode -1表示trimStart,0表示trim全部, 1表示trimEnd + * + * @return 除去指定字符后的的字符串,如果原字串为null,则返回null + */ + public static String trim(CharSequence str, int mode) { + if (str == null) { + return null; + } + + int length = str.length(); + int start = 0; + int end = length; + + // 扫描字符串头部 + if (mode <= 0) { + while ((start < end) && (CharUtil.isBlankChar(str.charAt(start)))) { + start++; + } + } + + // 扫描字符串尾部 + if (mode >= 0) { + while ((start < end) && (CharUtil.isBlankChar(str.charAt(end - 1)))) { + end--; + } + } + + if ((start > 0) || (end < length)) { + return str.toString().substring(start, end); + } + + return str.toString(); + } + + /** + * 字符串是否以给定字符开始 + * + * @param str 字符串 + * @param c 字符 + * @return 是否开始 + */ + public static boolean startWith(CharSequence str, char c) { + return c == str.charAt(0); + } + + /** + * 是否以指定字符串开头
+ * 如果给定的字符串和开头字符串都为null则返回true,否则任意一个值为null返回false + * + * @param str 被监测字符串 + * @param prefix 开头字符串 + * @param isIgnoreCase 是否忽略大小写 + * @return 是否以指定字符串开头 + */ + public static boolean startWith(CharSequence str, CharSequence prefix, boolean isIgnoreCase) { + if (null == str || null == prefix) { + if (null == str && null == prefix) { + return true; + } + return false; + } + + if (isIgnoreCase) { + return str.toString().toLowerCase().startsWith(prefix.toString().toLowerCase()); + } else { + return str.toString().startsWith(prefix.toString()); + } + } + + /** + * 是否以指定字符串开头 + * + * @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 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; + } + + /** + * 字符串是否以给定字符结尾 + * + * @param str 字符串 + * @param c 字符 + * @return 是否结尾 + */ + public static boolean endWith(CharSequence str, char c) { + return c == str.charAt(str.length() - 1); + } + + /** + * 是否以指定字符串结尾
+ * 如果给定的字符串和开头字符串都为null则返回true,否则任意一个值为null返回false + * + * @param str 被监测字符串 + * @param suffix 结尾字符串 + * @param isIgnoreCase 是否忽略大小写 + * @return 是否以指定字符串结尾 + */ + public static boolean endWith(CharSequence str, CharSequence suffix, boolean isIgnoreCase) { + if (null == str || null == suffix) { + if (null == str && null == suffix) { + return true; + } + return false; + } + + if (isIgnoreCase) { + return str.toString().toLowerCase().endsWith(suffix.toString().toLowerCase()); + } else { + return str.toString().endsWith(suffix.toString()); + } + } + + /** + * 是否以指定字符串结尾 + * + * @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; + } + + /** + * 指定字符是否在字符串中出现过 + * + * @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 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 (false == 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 (false == isEmpty(str)) { + int len = str.length(); + for (int i = 0; i < len; i++) { + if (false == ArrayUtil.contains(testChars, str.charAt(i))) { + 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; + } + + /** + * 是否包含特定字符,忽略大小写,如果给定两个参数都为null,返回true + * + * @param str 被检测字符串 + * @param testStr 被测试是否包含的字符串 + * @return 是否包含 + */ + public static boolean containsIgnoreCase(CharSequence str, CharSequence testStr) { + if (null == str) { + // 如果被监测字符串和 + return null == testStr; + } + return str.toString().toLowerCase().contains(testStr.toString().toLowerCase()); + } + + /** + * 查找指定字符串是否包含指定字符串列表中的任意一个字符串
+ * 忽略大小写 + * + * @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; + } + + /** + * 获得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"); + } + + /** + * 移除字符串中所有给定字符串
+ * 例:removeAll("aa-bb-cc-dd", "-") =》 aabbccdd + * + * @param str 字符串 + * @param strToRemove 被移除的字符串 + * @return 移除后的字符串 + */ + public static String removeAll(CharSequence str, CharSequence strToRemove) { + if (isEmpty(str)) { + return str(str); + } + return str.toString().replace(strToRemove, EMPTY); + } + + /** + * 去除字符串中指定的多个字符,如有多个则全部去除 + * + * @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 = builder(len); + char c; + for (int i = 0; i < len; i++) { + c = str.charAt(i); + if (false == 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, C_CR, C_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)); + } + + /** + * 原字符串首字母大写并在其首部添加指定字符串 例如: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(); + } + + /** + * 去掉指定前缀 + * + * @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 (str2.toLowerCase().startsWith(prefix.toString().toLowerCase())) { + 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 (str2.toLowerCase().endsWith(suffix.toString().toLowerCase())) { + return subPre(str2, str2.length() - suffix.length()); + } + return str2; + } + + /** + * 去除两边的指定字符串 + * + * @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); + } + + /** + * 如果给定字符串不是以prefix开头的,在开头补充 prefix + * + * @param str 字符串 + * @param prefix 前缀 + * @return 补充后的字符串 + */ + public static String addPrefixIfNot(CharSequence str, CharSequence prefix) { + if (isEmpty(str) || isEmpty(prefix)) { + return str(str); + } + + final String str2 = str.toString(); + final String prefix2 = prefix.toString(); + if (false == str2.startsWith(prefix2)) { + return prefix2.concat(str2); + } + return str2; + } + + /** + * 如果给定字符串不是以suffix结尾的,在尾部补充 suffix + * + * @param str 字符串 + * @param suffix 后缀 + * @return 补充后的字符串 + */ + public static String addSuffixIfNot(CharSequence str, CharSequence suffix) { + if (isEmpty(str) || isEmpty(suffix)) { + return str(str); + } + + final String str2 = str.toString(); + final String suffix2 = suffix.toString(); + if (false == str2.endsWith(suffix2)) { + return str2.concat(suffix2); + } + return str2; + } + + /** + * 清理空白字符 + * + * @param str 被清理的字符串 + * @return 清理后的字符串 + */ + public static String cleanBlank(CharSequence str) { + if (str == null) { + return null; + } + + int len = str.length(); + final StringBuilder sb = new StringBuilder(len); + char c; + for (int i = 0; i < len; i++) { + c = str.charAt(i); + if (false == CharUtil.isBlankChar(c)) { + sb.append(c); + } + } + return sb.toString(); + } + + // ------------------------------------------------------------------------------ Split + /** + * 切分字符串 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @return 切分后的数组 + */ + public static String[] splitToArray(CharSequence str, char separator) { + return splitToArray(str, separator, 0); + } + + /** + * 切分字符串为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 分隔符字符 + * @param limit 限制分片数 + * @return 切分后的数组 + */ + public static String[] splitToArray(CharSequence str, char separator, int limit) { + if (null == str) { + return new String[] {}; + } + return StrSpliter.splitToArray(str.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) { + if (null == str) { + return new ArrayList<>(0); + } + return StrSpliter.split(str.toString(), separator, limit, 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) { + if (null == str) { + return new ArrayList<>(0); + } + final String separatorStr = (null == separator) ? null : separator.toString(); + return StrSpliter.split(str.toString(), separatorStr, limit, isTrim, ignoreEmpty); + } + + /** + * 切分字符串 + * + * @param str 被切分的字符串 + * @param separator 分隔符 + * @return 字符串 + */ + public static String[] split(CharSequence str, CharSequence separator) { + if (str == null) { + return new String[] {}; + } + + final String separatorStr = (null == separator) ? null : separator.toString(); + return StrSpliter.splitToArray(str.toString(), separatorStr, 0, false, false); + } + + /** + * 根据给定长度,将给定字符串截取为多个部分 + * + * @param str 字符串 + * @param len 每一个小节的长度 + * @return 截取后的字符串数组 + * @see StrSpliter#splitByLength(String, int) + */ + public static String[] split(CharSequence str, int len) { + if (null == str) { + return new String[] {}; + } + return StrSpliter.splitByLength(str.toString(), len); + } + + /** + * 改进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 fromIndex 开始的index(包括) + * @param toIndex 结束的index(不包括) + * @return 字串 + */ + public static String sub(CharSequence str, int fromIndex, int toIndex) { + if (isEmpty(str)) { + return str(str); + } + int len = str.length(); + + 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 EMPTY; + } + + return str.toString().substring(fromIndex, toIndex); + } + + /** + * 截取部分字符串,这里一个汉字的长度认为是2 + * + * @param str 字符串 + * @param len 切割的位置 + * @param suffix 切割后加上后缀 + * @return 切割后的字符串 + * @since 3.1.1 + */ + public static String subPreGbk(CharSequence str, int len, CharSequence suffix) { + if (isEmpty(str)) { + return str(str); + } + + byte[] b; + int counterOfDoubleByte = 0; + b = str.toString().getBytes(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) { + len += 1; + } + return new String(b, 0, len, CharsetUtil.CHARSET_GBK) + suffix; + } + + /** + * 限制字符串长度,如果超过指定长度,截取指定长度并在末尾加"..." + * + * @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) + "..."; + } + + /** + * 切割指定位置之前部分的字符串 + * + * @param string 字符串 + * @param toIndex 切割到的位置(不包括) + * @return 切割后的剩余的前半部分字符串 + */ + public static String subPre(CharSequence string, int toIndex) { + return sub(string, 0, toIndex); + } + + /** + * 切割指定位置之后部分的字符串 + * + * @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()); + } + + /** + * 截取字符串,从指定位置开始,截取指定长度的字符串
+ * author weibaohui + * + * @param input 原始字符串 + * @param fromIndex 开始的index,包括 + * @param length 要截取的长度 + * @return 截取后的字符串 + */ + public static String subWithLength(String input, int fromIndex, int length) { + return sub(input, fromIndex, fromIndex + length); + } + + /** + * 截取分隔字符串之前的字符串,不包括分隔字符串
+ * 如果给定的字符串为空串(null或"")或者分隔字符串为null,返回原字符串
+ * 如果分隔字符串为空串"",则返回空串,如果分隔字符串未找到,返回原字符串,举例如下: + * + *
+	 * StrUtil.subBefore(null, *)      = null
+	 * StrUtil.subBefore("", *)        = ""
+	 * StrUtil.subBefore("abc", "a")   = ""
+	 * StrUtil.subBefore("abcba", "b") = "a"
+	 * StrUtil.subBefore("abc", "c")   = "ab"
+	 * StrUtil.subBefore("abc", "d")   = "abc"
+	 * StrUtil.subBefore("abc", "")    = ""
+	 * StrUtil.subBefore("abc", null)  = "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, *)      = null
+	 * StrUtil.subBefore("", *)        = ""
+	 * StrUtil.subBefore("abc", 'a')   = ""
+	 * StrUtil.subBefore("abcba", 'b') = "a"
+	 * StrUtil.subBefore("abc", 'c')   = "ab"
+	 * StrUtil.subBefore("abc", 'd')   = "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 : string.toString(); + } + + 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, *)      = null
+	 * StrUtil.subAfter("", *)        = ""
+	 * StrUtil.subAfter(*, null)      = ""
+	 * StrUtil.subAfter("abc", "a")   = "bc"
+	 * StrUtil.subAfter("abcba", "b") = "cba"
+	 * StrUtil.subAfter("abc", "c")   = ""
+	 * StrUtil.subAfter("abc", "d")   = ""
+	 * StrUtil.subAfter("abc", "")    = "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 : string.toString(); + } + 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, *)      = null
+	 * StrUtil.subAfter("", *)        = ""
+	 * StrUtil.subAfter("abc", 'a')   = "bc"
+	 * StrUtil.subAfter("abcba", 'b') = "cba"
+	 * StrUtil.subAfter("abc", 'c')   = ""
+	 * StrUtil.subAfter("abc", 'd')   = ""
+	 * 
+ * + * @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 : string.toString(); + } + 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); + } + + /** + * 给定字符串是否被字符包围 + * + * @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; + } + + /** + * 重复某个字符 + * + * @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) { + return EMPTY; + } + if (count == 1 || str.length() == 0) { + 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 conjunction 分界符 + * @return 连接后的字符串 + * @since 4.0.1 + */ + public static String repeatAndJoin(CharSequence str, int count, CharSequence conjunction) { + if (count <= 0) { + return EMPTY; + } + final StrBuilder builder = StrBuilder.create(); + boolean isFirst = true; + while (count-- > 0) { + if (isFirst) { + isFirst = false; + } else if (isNotEmpty(conjunction)) { + builder.append(conjunction); + } + builder.append(str); + } + return builder.toString(); + } + + /** + * 比较两个字符串(大小写敏感)。 + * + *
+	 * 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 如果两个字符串相同,或者都是null,则返回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 如果两个字符串相同,或者都是null,则返回true + */ + public static boolean equalsIgnoreCase(CharSequence str1, CharSequence str2) { + return equals(str1, str2, true); + } + + /** + * 比较两个字符串是否相等。 + * + * @param str1 要比较的字符串1 + * @param str2 要比较的字符串2 + * @param ignoreCase 是否忽略大小写 + * @return 如果两个字符串相同,或者都是null,则返回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.equals(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; + } + + /** + * 格式化文本, {} 表示占位符
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用: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 文本模板,被替换的部分用 {} 表示 + * @param params 参数值 + * @return 格式化后的文本 + */ + 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); + } + + /** + * 格式化文本,使用 {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) { + if (null == template) { + return null; + } + if (null == map || map.isEmpty()) { + return template.toString(); + } + + String template2 = template.toString(); + String value; + for (Entry entry : map.entrySet()) { + value = utf8Str(entry.getValue()); + if (null != value) { + template2 = replace(template2, "{" + entry.getKey() + "}", value); + } + } + return template2; + } + + /** + * 编码字符串,编码为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); + } + + /** + * 将对象转为字符串
+ * 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 字符串 + */ + 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, isBlank(charset) ? Charset.defaultCharset() : Charset.forName(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, isBlank(charset) ? Charset.defaultCharset() : Charset.forName(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.byteValue(); + } + + 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, Charset.forName(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(); + } + + /** + * {@link CharSequence} 转为字符串,null安全 + * + * @param cs {@link CharSequence} + * @return 字符串 + */ + public static String str(CharSequence cs) { + return null == cs ? null : cs.toString(); + } + + /** + * 调用对象的toString方法,null会返回“null” + * + * @param obj 对象 + * @return 字符串 + * @since 4.1.3 + */ + public static String toString(Object obj) { + return null == obj ? NULL : obj.toString(); + } + + /** + * 字符串转换为byteBuffer + * + * @param str 字符串 + * @param charset 编码 + * @return byteBuffer + */ + public static ByteBuffer byteBuffer(CharSequence str, String charset) { + return ByteBuffer.wrap(bytes(str, charset)); + } + + /** + * 以 conjunction 为分隔符将多个对象转换为字符串 + * + * @see ArrayUtil#join(Object, CharSequence) + * + * @param conjunction 分隔符 + * @param objs 数组 + * @return 连接后的字符串 + */ + public static String join(CharSequence conjunction, Object... objs) { + return ArrayUtil.join(objs, conjunction); + } + + /** + * 将驼峰式命名的字符串转换为下划线方式。如果转换前的驼峰式命名的字符串为空,则返回空字符串。
+ * 例如: + * + *
+	 * 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); + } + + /** + * 将驼峰式命名的字符串转换为使用符号连接方式。如果转换前的驼峰式命名的字符串为空,则返回空字符串。
+ * + * @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 StringBuilder sb = new StringBuilder(); + char c; + for (int i = 0; i < length; i++) { + c = str.charAt(i); + final Character preChar = (i > 0) ? str.charAt(i - 1) : null; + if (Character.isUpperCase(c)) { + // 遇到大写字母处理 + final Character nextChar = (i < str.length() - 1) ? str.charAt(i + 1) : null; + if (null != preChar && Character.isUpperCase(preChar)) { + // 前一个字符为大写,则按照一个词对待 + sb.append(c); + } else if (null != nextChar && Character.isUpperCase(nextChar)) { + // 后一个为大写字母,按照一个词对待 + if (null != preChar && symbol != preChar) { + // 前一个是非大写时按照新词对待,加连接符 + sb.append(symbol); + } + sb.append(c); + } else { + // 前后都为非大写按照新词对待 + if (null != preChar && symbol != preChar) { + // 前一个非连接符,补充连接符 + sb.append(symbol); + } + sb.append(Character.toLowerCase(c)); + } + } else { + if (sb.length() > 0 && Character.isUpperCase(sb.charAt(sb.length() - 1)) && symbol != c) { + // 当结果中前一个字母为大写,当前为小写,说明此字符为新词开始(连接符也表示新词) + sb.append(symbol); + } + // 小写或符号 + sb.append(c); + } + } + return sb.toString(); + } + + /** + * 将下划线方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。
+ * 例如:hello_world=》helloWorld + * + * @param name 转换前的下划线大写方式命名的字符串 + * @return 转换后的驼峰式命名的字符串 + */ + public static String toCamelCase(CharSequence name) { + if (null == name) { + return null; + } + + String name2 = name.toString(); + if (name2.contains(UNDERLINE)) { + final StringBuilder sb = new StringBuilder(name2.length()); + boolean upperCase = false; + for (int i = 0; i < name2.length(); i++) { + char c = name2.charAt(i); + + if (c == CharUtil.UNDERLINE) { + upperCase = true; + } else if (upperCase) { + sb.append(Character.toUpperCase(c)); + upperCase = false; + } else { + sb.append(Character.toLowerCase(c)); + } + } + return sb.toString(); + } else { + return name2; + } + } + + /** + * 包装指定字符串
+ * 当前缀和后缀一致时使用此方法 + * + * @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 4.0.7 + */ + public static String[] wrapAll(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 += str.length(); + } + if (isNotEmpty(suffix)) { + len += str.length(); + } + StringBuilder sb = new StringBuilder(len); + if (isNotEmpty(prefix) && false == startWith(str, prefix)) { + sb.append(prefix); + } + if (isNotEmpty(str)) { + sb.append(str); + } + if (isNotEmpty(suffix) && false == endWith(str, suffix)) { + sb.append(suffix); + } + return sb.toString(); + } + + /** + * 包装多个字符串,如果已经包装,则不再包装 + * + * @param prefixAndSuffix 前缀和后缀 + * @param strs 多个字符串 + * @return 包装的字符串数组 + * @since 4.0.7 + */ + public static String[] wrapAllIfMissing(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; + } + + /** + * 补充字符串以满足最小长度 + * + *
+	 * StrUtil.padPre(null, *, *);//null
+	 * StrUtil.padPre("1", 3, "ABC");//"AB1"
+	 * StrUtil.padPre("123", 2, "ABC");//"12"
+	 * 
+ * + * @param str 字符串 + * @param minLength 最小长度 + * @param padStr 补充的字符 + * @return 补充后的字符串 + */ + public static String padPre(CharSequence str, int minLength, CharSequence padStr) { + if (null == str) { + return null; + } + final int strLen = str.length(); + if (strLen == minLength) { + return str.toString(); + } else if (strLen > minLength) { + return subPre(str, minLength); + } + + return repeatByLength(padStr, minLength - strLen).concat(str.toString()); + } + + /** + * 补充字符串以满足最小长度 + * + *
+	 * StrUtil.padPre(null, *, *);//null
+	 * StrUtil.padPre("1", 3, '0');//"001"
+	 * StrUtil.padPre("123", 2, '0');//"12"
+	 * 
+ * + * @param str 字符串 + * @param minLength 最小长度 + * @param padChar 补充的字符 + * @return 补充后的字符串 + */ + public static String padPre(CharSequence str, int minLength, char padChar) { + if (null == str) { + return null; + } + final int strLen = str.length(); + if (strLen == minLength) { + return str.toString(); + } else if (strLen > minLength) { + return subPre(str, minLength); + } + + return repeat(padChar, minLength - strLen).concat(str.toString()); + } + + /** + * 补充字符串以满足最小长度 + * + *
+	 * StrUtil.padAfter(null, *, *);//null
+	 * StrUtil.padAfter("1", 3, '0');//"100"
+	 * StrUtil.padAfter("123", 2, '0');//"23"
+	 * 
+ * + * @param str 字符串,如果为null,按照空串处理 + * @param minLength 最小长度 + * @param padChar 补充的字符 + * @return 补充后的字符串 + */ + public static String padAfter(CharSequence str, int minLength, char padChar) { + if (null == str) { + return null; + } + final int strLen = str.length(); + if (strLen == minLength) { + return str.toString(); + } else if (strLen > minLength) { + return sub(str, strLen - minLength, strLen); + } + + return str.toString().concat(repeat(padChar, minLength - strLen)); + } + + /** + * 补充字符串以满足最小长度 + * + *
+	 * StrUtil.padAfter(null, *, *);//null
+	 * StrUtil.padAfter("1", 3, "ABC");//"1AB"
+	 * StrUtil.padAfter("123", 2, "ABC");//"23"
+	 * 
+ * + * @param str 字符串,如果为null,按照空串处理 + * @param minLength 最小长度 + * @param padStr 补充的字符 + * @return 补充后的字符串 + * @since 4.3.2 + */ + public static String padAfter(CharSequence str, int minLength, CharSequence padStr) { + if (null == str) { + return null; + } + final int strLen = str.length(); + if (strLen == minLength) { + return str.toString(); + } else if (strLen > minLength) { + return subSufByLength(str, minLength); + } + + return str.toString().concat(repeatByLength(padStr, minLength - strLen)); + } + + /** + * 居中字符串,两边补充指定字符串,如果指定长度小于字符串,则返回原字符串 + * + *
+	 * 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(); + } + + /** + * 创建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); + } + + /** + * 创建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); + } + + /** + * 获得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(); + } + + /** + * 统计指定内容中包含指定字符串的数量
+ * 参数为 {@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; + } + + /** + * 将字符串切分为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; + } + + /** + * 将给定字符串,变成 "xxx...xxx" 形式的字符串 + * + * @param str 字符串 + * @param maxLength 最大长度 + * @return 截取后的字符串 + */ + public static String brief(CharSequence str, int maxLength) { + if (null == str) { + return null; + } + if ((str.length() + 3) <= maxLength) { + return str.toString(); + } + int w = maxLength / 2; + int l = str.length(); + + final String str2 = str.toString(); + return format("{}...{}", str2.substring(0, maxLength - w), str2.substring(l - w)); + } + + /** + * 比较两个字符串,用于排序 + * + *
+	 * 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)); + } + + /** + * 指定范围内查找指定字符 + * + * @param str 字符串 + * @param searchChar 被查找的字符 + * @return 位置 + */ + public static int indexOf(final CharSequence str, char searchChar) { + return indexOf(str, searchChar, 0); + } + + /** + * 指定范围内查找指定字符 + * + * @param str 字符串 + * @param searchChar 被查找的字符 + * @param start 起始位置,如果小于0,从0开始查找 + * @return 位置 + */ + public static int indexOf(final CharSequence str, char searchChar, int start) { + if (str instanceof String) { + return ((String) str).indexOf(searchChar, start); + } else { + return indexOf(str, searchChar, start, -1); + } + } + + /** + * 指定范围内查找指定字符 + * + * @param str 字符串 + * @param searchChar 被查找的字符 + * @param start 起始位置,如果小于0,从0开始查找 + * @param end 终止位置,如果超过str.length()则默认查找到字符串末尾 + * @return 位置 + */ + public static int indexOf(final CharSequence str, char searchChar, int start, int end) { + final int len = str.length(); + if (start < 0 || start > len) { + start = 0; + } + if (end > len || end < 0) { + end = len; + } + for (int i = start; i < end; i++) { + if (str.charAt(i) == searchChar) { + return i; + } + } + return -1; + } + + /** + * 指定范围内查找字符串,忽略大小写
+ * + *
+	 * 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 str 字符串 + * @param searchStr 需要查找位置的字符串 + * @param fromIndex 起始位置 + * @param ignoreCase 是否忽略大小写 + * @return 位置 + * @since 3.2.1 + */ + public static int indexOf(final CharSequence str, CharSequence searchStr, int fromIndex, boolean ignoreCase) { + if (str == null || searchStr == null) { + return INDEX_NOT_FOUND; + } + if (fromIndex < 0) { + fromIndex = 0; + } + + final int endLimit = str.length() - searchStr.length() + 1; + if (fromIndex > endLimit) { + return INDEX_NOT_FOUND; + } + if (searchStr.length() == 0) { + return fromIndex; + } + + if (false == ignoreCase) { + // 不忽略大小写调用JDK方法 + return str.toString().indexOf(searchStr.toString(), fromIndex); + } + + for (int i = fromIndex; i < endLimit; i++) { + if (isSubEquals(str, i, searchStr, 0, searchStr.length(), true)) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + * 指定范围内查找字符串,忽略大小写 + * + * @param str 字符串 + * @param searchStr 需要查找位置的字符串 + * @return 位置 + * @since 3.2.1 + */ + public static int lastIndexOfIgnoreCase(final CharSequence str, final CharSequence searchStr) { + return lastIndexOfIgnoreCase(str, searchStr, str.length()); + } + + /** + * 指定范围内查找字符串,忽略大小写
+ * fromIndex 为搜索起始位置,从后往前计数 + * + * @param str 字符串 + * @param searchStr 需要查找位置的字符串 + * @param fromIndex 起始位置,从后往前计数 + * @return 位置 + * @since 3.2.1 + */ + public static int lastIndexOfIgnoreCase(final CharSequence str, final CharSequence searchStr, int fromIndex) { + return lastIndexOf(str, searchStr, fromIndex, true); + } + + /** + * 指定范围内查找字符串
+ * fromIndex 为搜索起始位置,从后往前计数 + * + * @param str 字符串 + * @param searchStr 需要查找位置的字符串 + * @param fromIndex 起始位置,从后往前计数 + * @param ignoreCase 是否忽略大小写 + * @return 位置 + * @since 3.2.1 + */ + public static int lastIndexOf(final CharSequence str, final CharSequence searchStr, int fromIndex, boolean ignoreCase) { + if (str == null || searchStr == null) { + return INDEX_NOT_FOUND; + } + if (fromIndex < 0) { + fromIndex = 0; + } + fromIndex = Math.min(fromIndex, str.length()); + + if (searchStr.length() == 0) { + return fromIndex; + } + + if (false == ignoreCase) { + // 不忽略大小写调用JDK方法 + return str.toString().lastIndexOf(searchStr.toString(), fromIndex); + } + + for (int i = fromIndex; i > 0; i--) { + if (isSubEquals(str, i, searchStr, 0, searchStr.length(), true)) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + * 返回字符串 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(String str, String 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 = str.indexOf(searchStr, index + 1); + if (index < 0) { + return index; + } + found++; + } while (found < ordinal); + return index; + } + + // ------------------------------------------------------------------------------------------------------------------ Append and prepend + /** + * 如果给定字符串不是以给定的一个或多个字符串为结尾,则在尾部添加结尾字符串
+ * 不忽略大小写 + * + * @param str 被检查的字符串 + * @param suffix 需要添加到结尾的字符串 + * @param suffixes 需要额外检查的结尾字符串,如果以这些中的一个为结尾,则不再添加 + * + * @return 如果已经结尾,返回原字符串,否则返回添加结尾的字符串 + * @since 3.0.7 + */ + public static String appendIfMissing(final CharSequence str, final CharSequence suffix, final CharSequence... suffixes) { + return appendIfMissing(str, suffix, false, suffixes); + } + + /** + * 如果给定字符串不是以给定的一个或多个字符串为结尾,则在尾部添加结尾字符串
+ * 忽略大小写 + * + * @param str 被检查的字符串 + * @param suffix 需要添加到结尾的字符串 + * @param suffixes 需要额外检查的结尾字符串,如果以这些中的一个为结尾,则不再添加 + * + * @return 如果已经结尾,返回原字符串,否则返回添加结尾的字符串 + * @since 3.0.7 + */ + public static String appendIfMissingIgnoreCase(final CharSequence str, final CharSequence suffix, final CharSequence... suffixes) { + return appendIfMissing(str, suffix, true, suffixes); + } + + /** + * 如果给定字符串不是以给定的一个或多个字符串为结尾,则在尾部添加结尾字符串 + * + * @param str 被检查的字符串 + * @param suffix 需要添加到结尾的字符串 + * @param ignoreCase 检查结尾时是否忽略大小写 + * @param suffixes 需要额外检查的结尾字符串,如果以这些中的一个为结尾,则不再添加 + * + * @return 如果已经结尾,返回原字符串,否则返回添加结尾的字符串 + * @since 3.0.7 + */ + public static String appendIfMissing(final CharSequence str, final CharSequence suffix, final boolean ignoreCase, final CharSequence... suffixes) { + if (str == null || isEmpty(suffix) || endWith(str, suffix, ignoreCase)) { + return str(str); + } + if (suffixes != null && suffixes.length > 0) { + for (final CharSequence s : suffixes) { + if (endWith(str, s, 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(final CharSequence str, final CharSequence prefix, final CharSequence... prefixes) { + return prependIfMissing(str, prefix, false, prefixes); + } + + /** + * 如果给定字符串不是以给定的一个或多个字符串为开头,则在首部添加起始字符串
+ * 忽略大小写 + * + * @param str 被检查的字符串 + * @param prefix 需要添加到首部的字符串 + * @param prefixes 需要额外检查的首部字符串,如果以这些中的一个为起始,则不再添加 + * + * @return 如果已经结尾,返回原字符串,否则返回添加结尾的字符串 + * @since 3.0.7 + */ + public static String prependIfMissingIgnoreCase(final CharSequence str, final CharSequence prefix, final 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(final CharSequence str, final CharSequence prefix, final boolean ignoreCase, final CharSequence... prefixes) { + if (str == null || isEmpty(prefix) || startWith(str, prefix, ignoreCase)) { + return str(str); + } + if (prefixes != null && prefixes.length > 0) { + for (final CharSequence s : prefixes) { + if (startWith(str, s, ignoreCase)) { + return str.toString(); + } + } + } + return prefix.toString().concat(str.toString()); + } + + /** + * 反转字符串
+ * 例如:abcd =》dcba + * + * @param str 被反转的字符串 + * @return 反转后的字符串 + * @since 3.0.9 + */ + public static String reverse(String str) { + return new String(ArrayUtil.reverse(str.toCharArray())); + } + + /** + * 将已有字符串填充为规定长度,如果已有字符串超过这个长度则返回这个字符串
+ * 字符填充于字符串前 + * + * @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); + } + + /** + * 截取两个字符串的不同部分(长度一致),判断截取的子串是否相同
+ * 任意一个字符串为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); + } + + /** + * 字符串的每一个字符是否都与定义的匹配器匹配 + * + * @param value 字符串 + * @param matcher 匹配器 + * @return 是否全部匹配 + * @since 3.2.3 + */ + public static boolean isAllCharMatch(CharSequence value, Matcher matcher) { + if (StrUtil.isBlank(value)) { + return false; + } + int len = value.length(); + boolean isAllMatch = true; + for (int i = 0; i < len; i++) { + isAllMatch &= matcher.match(value.charAt(i)); + } + return isAllMatch; + } + + /** + * 替换字符串中的指定字符串,忽略大小写 + * + * @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 (fromIndex > strLength) { + return str(str); + } else if (fromIndex < 0) { + fromIndex = 0; + } + + final StrBuilder result = StrBuilder.create(strLength + 16); + if (0 != fromIndex) { + result.append(str.subSequence(0, fromIndex)); + } + + int preIndex = fromIndex; + int index = fromIndex; + 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(); + } + + /** + * 替换指定字符串的指定区间内字符为固定字符 + * + * @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 int strLength = str.length(); + if (startInclude > strLength) { + return str(str); + } + if (endExclude > strLength) { + endExclude = strLength; + } + if (startInclude > endExclude) { + // 如果起始位置大于结束位置,不替换 + return str(str); + } + + final char[] chars = new char[strLength]; + for (int i = 0; i < strLength; i++) { + if (i >= startInclude && i < endExclude) { + chars[i] = replacedChar; + } else { + chars[i] = str.charAt(i); + } + } + return new String(chars); + } + + /** + * 替换所有正则匹配的文本,并使用自定义函数决定如何替换 + * + * @param str 要替换的字符串 + * @param pattern 用于匹配的正则式 + * @param replaceFun 决定如何替换的函数 + * @return 替换后的字符串 + * @see ReUtil#replaceAll(CharSequence, Pattern, Func1) + * @since 4.2.2 + */ + public static String replace(CharSequence str, 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 startInclude 开始位置(包含) + * @param endExclude 结束位置(不包含) + * @return 替换后的字符串 + * @since 4.1.14 + */ + public static String hide(CharSequence str, int startInclude, int endExclude) { + return replace(str, startInclude, endExclude, '*'); + } + + /** + * 替换字符字符数组中所有的字符为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 = builder(); + char c; + for (int i = 0; i < strLen; i++) { + c = str.charAt(i); + builder.append(set.contains(c) ? replacedStr : c); + } + return builder.toString(); + } + + /** + * 计算两个字符串的相似度 + * + * @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); + } + + /** + * 字符串指定位置的字符是否与给定字符相同
+ * 如果字符串为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字符长度定义为0 + * + * @param strs 字符串数组 + * @return 总长度 + * @since 4.0.1 + */ + public static int totalLength(CharSequence... strs) { + int totalLength = 0; + for (int i = 0; i < strs.length; i++) { + totalLength += (null == strs[i] ? 0 : strs[i].length()); + } + return totalLength; + } + + /** + * 循环位移指定位置的字符串为指定距离
+ * 当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 StrBuilder strBuilder = StrBuilder.create(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(); + } + + /** + * 生成随机UUID + * + * @return UUID字符串 + * @since 4.0.10 + * @see IdUtil#randomUUID() + */ + public static String uuid() { + return IdUtil.randomUUID(); + } + + /** + * 连接多个字符串为一个 + * + * @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(); + } + + /** + * 给定字符串中的字母是否全部为大写,判断依据如下: + * + *
+	 * 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; + } + + /** + * 获取字符串的长度,如果为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; + } + + /** + * 切换给定字符串中的大小写。大写转小写,小写转大写。 + * + *
+	 * 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); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/util/TypeUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/TypeUtil.java new file mode 100644 index 000000000..0fdcffbe3 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/util/TypeUtil.java @@ -0,0 +1,359 @@ +package cn.hutool.core.util; + +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; + +import cn.hutool.core.map.TableMap; + +/** + * 针对 {@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; + } + Type type = field.getGenericType(); + if (null == type) { + type = field.getType(); + } + return type; + } + + /** + * 获得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) { + if (type instanceof ParameterizedType) { + return (ParameterizedType) type; + } else if (type instanceof Class) { + return toParameterizedType(((Class) type).getGenericSuperclass()); + } + return null; + } + + /** + * 获取指定泛型变量对应的真实类型
+ * 由于子类中泛型参数实现和父类(接口)中泛型定义位置是一一对应的,因此可以通过对应关系找到泛型实现类型
+ * 使用此方法注意: + * + *
+	 * 1. superClass必须是clazz的父类或者clazz实现的接口
+	 * 2. typeVariable必须在superClass中声明
+	 * 
+ * + * + * @param actualType 真实类型所在类,此类中记录了泛型参数对应的实际类型 + * @param typeDefineClass 泛型变量声明所在类或接口,此类中定义了泛型类型 + * @param typeVariables 泛型变量,需要的实际类型对应的泛型参数 + * @return 给定泛型参数对应的实际类型,如果无对应类型,返回null + * @since 4.5.7 + */ + public static Type[] getActualTypes(Type actualType, Class typeDefineClass, Type... typeVariables) { + if (false == typeDefineClass.isAssignableFrom(getClass(actualType))) { + throw new IllegalArgumentException("Parameter [superClass] must be assignable from [clazz]"); + } + + // 泛型参数标识符列表 + final TypeVariable[] typeVars = typeDefineClass.getTypeParameters(); + if(ArrayUtil.isEmpty(typeVars)) { + return null; + } + // 实际类型列表 + final Type[] actualTypeArguments = TypeUtil.getTypeArguments(actualType); + if(ArrayUtil.isEmpty(actualTypeArguments)) { + return null; + } + + int size = Math.min(actualTypeArguments.length, typeVars.length); + final Map, Type> tableMap = new TableMap<>(typeVars, actualTypeArguments); + + // 查找方法定义所在类或接口中此泛型参数的位置 + final Type[] result = new Type[size]; + for(int i = 0; i < typeVariables.length; i++) { + result[i] = (typeVariables[i] instanceof TypeVariable) ? tableMap.get(typeVariables[i]) : typeVariables[i]; + } + return result; + } + + /** + * 获取指定泛型变量对应的真实类型
+ * 由于子类中泛型参数实现和父类(接口)中泛型定义位置是一一对应的,因此可以通过对应关系找到泛型实现类型
+ * 使用此方法注意: + * + *
+	 * 1. superClass必须是clazz的父类或者clazz实现的接口
+	 * 2. typeVariable必须在superClass中声明
+	 * 
+ * + * + * @param actualType 真实类型所在类,此类中记录了泛型参数对应的实际类型 + * @param typeDefineClass 泛型变量声明所在类或接口,此类中定义了泛型类型 + * @param typeVariable 泛型变量,需要的实际类型对应的泛型参数 + * @return 给定泛型参数对应的实际类型 + * @since 4.5.2 + */ + public static Type getActualType(Type actualType, Class typeDefineClass, Type typeVariable) { + Type[] types = getActualTypes(actualType, typeDefineClass, typeVariable); + if(ArrayUtil.isNotEmpty(types)) { + return types[0]; + } + return null; + } + + /** + * 是否未知类型
+ * type为null或者{@link TypeVariable} 都视为未知类型 + * + * @param type Type类型 + * @return 是否未知类型 + * @since 4.5.2 + */ + public static boolean isUnknow(Type type) { + return null == type || type instanceof TypeVariable; + } + + /** + * 指定泛型数组中是否含有泛型变量 + * @param types 泛型数组 + * @return 是否含有泛型变量 + * @since 4.5.7 + */ + public static boolean hasTypeVeriable(Type... types) { + for (Type type : types) { + if(type instanceof TypeVariable) { + return true; + } + } + return false; + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/util/URLUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/URLUtil.java new file mode 100644 index 000000000..887692f2e --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/util/URLUtil.java @@ -0,0 +1,630 @@ +package cn.hutool.core.util; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.JarURLConnection; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLDecoder; +import java.net.URLStreamHandler; +import java.nio.charset.Charset; +import java.util.jar.JarFile; + +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.URLEncoder; + +/** + * 统一资源定位符相关工具类 + * + * @author xiaoleilu + * + */ +public class URLUtil { + + /** 针对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 = "*/"; + + /** + * 通过一个字符串形式的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) { + Assert.notNull(url, "URL must not be 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); + } + } + } + + /** + * 将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 + * @exception 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 + * @exception 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; + } + + /** + * 补全相对路径 + * + * @param baseUrl 基准URL + * @param relativePath 相对URL + * @return 相对路径 + * @exception UtilException MalformedURLException + */ + public static String complateUrl(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); + } + } + + /** + * 编码URL,默认使用UTF-8编码
+ * 将需要转换的内容(ASCII码形式之外的内容),用十六进制表示法转换出来,并在之前加上%开头。 + * + * @param url URL + * @return 编码后的URL + * @exception UtilException UnsupportedEncodingException + */ + public static String encodeAll(String url) { + return encodeAll(url, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 编码URL
+ * 将需要转换的内容(ASCII码形式之外的内容),用十六进制表示法转换出来,并在之前加上%开头。 + * + * @param url URL + * @param charset 编码 + * @return 编码后的URL + * @exception UtilException UnsupportedEncodingException + */ + public static String encodeAll(String url, Charset charset) throws UtilException { + try { + return java.net.URLEncoder.encode(url, charset.toString()); + } catch (UnsupportedEncodingException e) { + throw new UtilException(e); + } + } + + /** + * 编码URL,默认使用UTF-8编码
+ * 将需要转换的内容(ASCII码形式之外的内容),用十六进制表示法转换出来,并在之前加上%开头。
+ * 此方法用于URL自动编码,类似于浏览器中键入地址自动编码,对于像类似于“/”的字符不再编码 + * + * @param url URL + * @return 编码后的URL + * @exception UtilException UnsupportedEncodingException + * @since 3.1.2 + */ + public static String encode(String url) throws UtilException { + return encode(url, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 编码URL,默认使用UTF-8编码
+ * 将需要转换的内容(ASCII码形式之外的内容),用十六进制表示法转换出来,并在之前加上%开头。
+ * 此方法用于POST请求中的请求体自动编码,转义大部分特殊字符 + * + * @param url URL + * @return 编码后的URL + * @exception UtilException UnsupportedEncodingException + * @since 3.1.2 + */ + public static String encodeQuery(String url) throws UtilException { + return encodeQuery(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) { + if (StrUtil.isEmpty(url)) { + return url; + } + if (null == charset) { + charset = CharsetUtil.defaultCharset(); + } + return URLEncoder.DEFAULT.encode(url, charset); + } + + /** + * 编码字符为URL中查询语句
+ * 将需要转换的内容(ASCII码形式之外的内容),用十六进制表示法转换出来,并在之前加上%开头。
+ * 此方法用于POST请求中的请求体自动编码,转义大部分特殊字符 + * + * @param url 被编码内容 + * @param charset 编码 + * @return 编码后的字符 + * @since 4.4.1 + */ + public static String encodeQuery(String url, Charset charset) { + if (StrUtil.isEmpty(url)) { + return url; + } + if (null == charset) { + charset = CharsetUtil.defaultCharset(); + } + return URLEncoder.QUERY.encode(url, charset); + } + + /** + * 编码URL字符为 application/x-www-form-urlencoded
+ * 将需要转换的内容(ASCII码形式之外的内容),用十六进制表示法转换出来,并在之前加上%开头。
+ * 此方法用于URL自动编码,类似于浏览器中键入地址自动编码,对于像类似于“/”的字符不再编码 + * + * @param url URL + * @param charset 编码 + * @return 编码后的URL + * @exception UtilException UnsupportedEncodingException + */ + public static String encode(String url, String charset) throws UtilException { + if (StrUtil.isEmpty(url)) { + return url; + } + return encode(url, StrUtil.isBlank(charset) ? CharsetUtil.defaultCharset() : CharsetUtil.charset(charset)); + } + + /** + * 编码URL
+ * 将需要转换的内容(ASCII码形式之外的内容),用十六进制表示法转换出来,并在之前加上%开头。
+ * 此方法用于POST请求中的请求体自动编码,转义大部分特殊字符 + * + * @param url URL + * @param charset 编码 + * @return 编码后的URL + * @exception UtilException UnsupportedEncodingException + */ + public static String encodeQuery(String url, String charset) throws UtilException { + return encodeQuery(url, StrUtil.isBlank(charset) ? CharsetUtil.defaultCharset() : CharsetUtil.charset(charset)); + } + + /** + * 解码URL
+ * 将%开头的16进制表示的内容解码。 + * + * @param url URL + * @return 解码后的URL + * @exception 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字符 + * + * @param content 被解码内容 + * @param charset 编码 + * @return 编码后的字符 + * @since 4.4.1 + */ + public static String decode(String content, Charset charset) { + if (null == charset) { + charset = CharsetUtil.defaultCharset(); + } + return decode(content, charset.name()); + } + + /** + * 解码URL
+ * 将%开头的16进制表示的内容解码。 + * + * @param url URL + * @param charset 编码 + * @return 解码后的URL + * @exception UtilException UnsupportedEncodingException + */ + public static String decode(String url, String charset) throws UtilException { + if (StrUtil.isEmpty(url)) { + return url; + } + try { + return URLDecoder.decode(url, charset); + } catch (UnsupportedEncodingException e) { + throw new UtilException(e, "Unsupported encoding: [{}]", charset); + } + } + + /** + * 获得path部分
+ * + * @param uriStr URI路径 + * @return path + * @exception UtilException 包装URISyntaxException + */ + public static String getPath(String uriStr) { + URI uri = null; + try { + uri = new URI(uriStr); + } catch (URISyntaxException e) { + throw new UtilException(e); + } + return uri.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 = URLUtil.toURI(url).getPath(); + } catch (UtilException e) { + // ignore + } + return (null != path) ? path : url.getPath(); + } + + /** + * 转URL为URI + * + * @param url URL + * @return URI + * @exception UtilException 包装URISyntaxException + */ + public static URI toURI(URL url) throws UtilException { + if (null == url) { + return null; + } + try { + return url.toURI(); + } catch (URISyntaxException e) { + throw new UtilException(e); + } + } + + /** + * 转字符串为URI + * + * @param location 字符串路径 + * @return URI + * @exception UtilException 包装URISyntaxException + */ + public static URI toURI(String location) throws UtilException { + try { + return new URI(location.replace(" ", "%20")); + } 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) { + 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) { + 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) { + 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); + 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. 多个/替换为一个
+	 * 
+ * + * @param url URL字符串 + * @return 标准化后的URL字符串 + */ + public static String normalize(String url) { + return normalize(url, false); + } + + /** + * 标准化URL字符串,包括: + * + *
+	 * 1. 多个/替换为一个
+	 * 
+ * + * @param url URL字符串 + * @param isEncodeBody 是否对URL中body部分的中文和特殊字符做转义(不包括http:和/) + * @return 标准化后的URL字符串 + * @since 4.4.1 + */ + public static String normalize(String url, boolean isEncodeBody) { + if (StrUtil.isBlank(url)) { + return url; + } + final int sepIndex = url.indexOf("://"); + String pre; + String body; + if (sepIndex > 0) { + pre = StrUtil.subPre(url, sepIndex + 3); + body = StrUtil.subSuf(url, sepIndex + 3); + } else { + pre = "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); + } + + // 去除开头的\或者/ + body = body.replaceAll("^[\\/]+", StrUtil.EMPTY); + // 替换多个\或/为单个/ + body = body.replace("\\", "/").replaceAll("//+", "/"); + if (isEncodeBody) { + body = encode(body); + } + return pre + body + StrUtil.nullToEmpty(params); + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/util/XmlUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/XmlUtil.java new file mode 100644 index 000000000..eae3bb044 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/util/XmlUtil.java @@ -0,0 +1,855 @@ +package cn.hutool.core.util; + +import java.beans.XMLDecoder; +import java.beans.XMLEncoder; +import java.io.BufferedInputStream; +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.StringWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import javax.xml.namespace.QName; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Result; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; + +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.lang.Assert; + +/** + * XML工具类
+ * 此工具使用w3c dom工具,不需要依赖第三方包。
+ * 工具类封装了XML文档的创建、读取、写出和部分XML操作 + * + * @author xiaoleilu + * + */ +public class XmlUtil { + + /** 在XML中无效的字符 正则 */ + public final static String INVALID_REGEX = "[\\x00-\\x08\\x0b-\\x0c\\x0e-\\x1f]"; + /** XML格式化输出默认缩进量 */ + public final static int INDENT_DEFAULT = 2; + + // -------------------------------------------------------------------------------------- Read + /** + * 读取解析XML文件 + * + * @param file XML文件 + * @return XML文档对象 + */ + public static Document readXML(File file) { + Assert.notNull(file, "Xml file is null !"); + if (false == file.exists()) { + throw new UtilException("File [{}] not a exist!", file.getAbsolutePath()); + } + if (false == file.isFile()) { + throw new UtilException("[{}] not a file!", file.getAbsolutePath()); + } + + try { + file = file.getCanonicalFile(); + } catch (IOException e) { + // ignore + } + + BufferedInputStream in = null; + try { + in = FileUtil.getInputStream(file); + return readXML(in); + } finally { + IoUtil.close(in); + } + } + + /** + * 读取解析XML文件
+ * 如果给定内容以“<”开头,表示这是一个XML内容,直接读取,否则按照路径处理
+ * 路径可以为相对路径,也可以是绝对路径,相对路径相对于ClassPath + * + * @param pathOrContent 内容或路径 + * @return XML文档对象 + * @since 3.0.9 + */ + public static Document readXML(String pathOrContent) { + if (StrUtil.startWith(pathOrContent, '<')) { + return parseXml(pathOrContent); + } + return readXML(FileUtil.file(pathOrContent)); + } + + /** + * 读取解析XML文件
+ * 编码在XML中定义 + * + * @param inputStream XML流 + * @return XML文档对象 + * @throws UtilException IO异常或转换异常 + * @since 3.0.9 + */ + public static Document readXML(InputStream inputStream) throws UtilException { + return readXML(new InputSource(inputStream)); + } + + /** + * 读取解析XML文件 + * + * @param reader XML流 + * @return XML文档对象 + * @throws UtilException IO异常或转换异常 + * @since 3.0.9 + */ + public static Document readXML(Reader reader) throws UtilException { + return readXML(new InputSource(reader)); + } + + /** + * 读取解析XML文件
+ * 编码在XML中定义 + * + * @param source {@link InputSource} + * @return XML文档对象 + * @since 3.0.9 + */ + public static Document readXML(InputSource source) { + final DocumentBuilder builder = createDocumentBuilder(); + try { + return builder.parse(source); + } catch (Exception e) { + throw new UtilException(e, "Parse XML from stream error!"); + } + } + + /** + * 将String类型的XML转换为XML文档 + * + * @param xmlStr XML字符串 + * @return XML文档 + */ + public static Document parseXml(String xmlStr) { + if (StrUtil.isBlank(xmlStr)) { + throw new IllegalArgumentException("XML content string is empty !"); + } + xmlStr = cleanInvalid(xmlStr); + return readXML(new InputSource(StrUtil.getReader(xmlStr))); + } + + /** + * 从XML中读取对象 Reads serialized object from the XML file. + * + * @param 对象类型 + * @param source XML文件 + * @return 对象 + * @throws IOException IO异常 + */ + public static T readObjectFromXml(File source) throws IOException { + return readObjectFromXml(new InputSource(FileUtil.getInputStream(source))); + } + + /** + * 从XML中读取对象 Reads serialized object from the XML file. + * + * @param 对象类型 + * @param xmlStr XML内容 + * @return 对象 + * @throws IOException IO异常 + * @since 3.2.0 + */ + public static T readObjectFromXml(String xmlStr) throws IOException { + return readObjectFromXml(new InputSource(StrUtil.getReader(xmlStr))); + } + + /** + * 从XML中读取对象 Reads serialized object from the XML file. + * + * @param 对象类型 + * @param source {@link InputSource} + * @return 对象 + * @throws IOException IO异常 + * @since 3.2.0 + */ + @SuppressWarnings("unchecked") + public static T readObjectFromXml(InputSource source) throws IOException { + Object result = null; + XMLDecoder xmldec = null; + try { + xmldec = new XMLDecoder(source); + result = xmldec.readObject(); + } finally { + IoUtil.close(xmldec); + } + return (T) result; + } + + // -------------------------------------------------------------------------------------- Write + /** + * 将XML文档转换为String
+ * 字符编码使用XML文档中的编码,获取不到则使用UTF-8
+ * 默认非格式化输出,若想格式化请使用{@link #format(Document)} + * + * @param doc XML文档 + * @return XML字符串 + */ + public static String toStr(Document doc) { + return toStr(doc, false); + } + + /** + * 将XML文档转换为String
+ * 字符编码使用XML文档中的编码,获取不到则使用UTF-8 + * + * @param doc XML文档 + * @param isPretty 是否格式化输出 + * @return XML字符串 + * @since 3.0.9 + */ + public static String toStr(Document doc, boolean isPretty) { + return toStr(doc, CharsetUtil.UTF_8, isPretty); + } + + /** + * 将XML文档转换为String
+ * 字符编码使用XML文档中的编码,获取不到则使用UTF-8 + * + * @param doc XML文档 + * @param charset 编码 + * @param isPretty 是否格式化输出 + * @return XML字符串 + * @since 3.0.9 + */ + public static String toStr(Document doc, String charset, boolean isPretty) { + final StringWriter writer = StrUtil.getWriter(); + try { + write(doc, writer, charset, isPretty ? INDENT_DEFAULT : 0); + } catch (Exception e) { + throw new UtilException(e, "Trans xml document to string error!"); + } + return writer.toString(); + } + + /** + * 格式化XML输出 + * + * @param doc {@link Document} XML文档 + * @return 格式化后的XML字符串 + * @since 4.4.5 + */ + public static String format(Document doc) { + return toStr(doc, true); + } + + /** + * 格式化XML输出 + * + * @param xmlStr XML字符串 + * @return 格式化后的XML字符串 + * @since 4.4.5 + */ + public static String format(String xmlStr) { + return format(parseXml(xmlStr)); + } + + /** + * 将XML文档写入到文件
+ * 使用Document中的编码 + * + * @param doc XML文档 + * @param absolutePath 文件绝对路径,不存在会自动创建 + */ + public static void toFile(Document doc, String absolutePath) { + toFile(doc, absolutePath, null); + } + + /** + * 将XML文档写入到文件
+ * + * @param doc XML文档 + * @param path 文件路径绝对路径或相对ClassPath路径,不存在会自动创建 + * @param charset 自定义XML文件的编码,如果为{@code null} 读取XML文档中的编码,否则默认UTF-8 + */ + public static void toFile(Document doc, String path, String charset) { + if (StrUtil.isBlank(charset)) { + charset = doc.getXmlEncoding(); + } + if (StrUtil.isBlank(charset)) { + charset = CharsetUtil.UTF_8; + } + + BufferedWriter writer = null; + try { + writer = FileUtil.getWriter(path, charset, false); + write(doc, writer, charset, INDENT_DEFAULT); + } finally { + IoUtil.close(writer); + } + } + + /** + * 将XML文档写出 + * + * @param node {@link Node} XML文档节点或文档本身 + * @param writer 写出的Writer,Writer决定了输出XML的编码 + * @param charset 编码 + * @param indent 格式化输出中缩进量,小于1表示不格式化输出 + * @since 3.0.9 + */ + public static void write(Node node, Writer writer, String charset, int indent) { + transform(new DOMSource(node), new StreamResult(writer), charset, indent); + } + + /** + * 将XML文档写出 + * + * @param node {@link Node} XML文档节点或文档本身 + * @param out 写出的Writer,Writer决定了输出XML的编码 + * @param charset 编码 + * @param indent 格式化输出中缩进量,小于1表示不格式化输出 + * @since 4.0.8 + */ + public static void write(Node node, OutputStream out, String charset, int indent) { + transform(new DOMSource(node), new StreamResult(out), charset, indent); + } + + /** + * 将XML文档写出
+ * 格式化输出逻辑参考:https://stackoverflow.com/questions/139076/how-to-pretty-print-xml-from-java + * + * @param source 源 + * @param result 目标 + * @param charset 编码 + * @param indent 格式化输出中缩进量,小于1表示不格式化输出 + * @since 4.0.9 + */ + public static void transform(Source source, Result result, String charset, int indent) { + final TransformerFactory factory = TransformerFactory.newInstance(); + try { + final Transformer xformer = factory.newTransformer(); + if (indent > 0) { + xformer.setOutputProperty(OutputKeys.INDENT, "yes"); + xformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", String.valueOf(indent)); + } + if (StrUtil.isNotBlank(charset)) { + xformer.setOutputProperty(OutputKeys.ENCODING, charset); + } + xformer.transform(source, result); + } catch (Exception e) { + throw new UtilException(e, "Trans xml document to string error!"); + } + } + + // -------------------------------------------------------------------------------------- Create + /** + * 创建XML文档
+ * 创建的XML默认是utf8编码,修改编码的过程是在toStr和toFile方法里,既XML在转为文本的时候才定义编码 + * + * @return XML文档 + * @since 4.0.8 + */ + public static Document createXml() { + return createDocumentBuilder().newDocument(); + } + + /** + * 创建 DocumentBuilder + * + * @return DocumentBuilder + * @since 4.1.2 + */ + public static DocumentBuilder createDocumentBuilder() { + final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + disableXXE(dbf); + DocumentBuilder builder = null; + try { + builder = dbf.newDocumentBuilder(); + } catch (Exception e) { + throw new UtilException(e, "Create xml document error!"); + } + return builder; + } + + /** + * 创建XML文档
+ * 创建的XML默认是utf8编码,修改编码的过程是在toStr和toFile方法里,既XML在转为文本的时候才定义编码 + * + * @param rootElementName 根节点名称 + * @return XML文档 + */ + public static Document createXml(String rootElementName) { + final Document doc = createXml(); + doc.appendChild(doc.createElement(rootElementName)); + + return doc; + } + + // -------------------------------------------------------------------------------------- Function + /** + * 获得XML文档根节点 + * + * @param doc {@link Document} + * @return 根节点 + * @see Document#getDocumentElement() + * @since 3.0.8 + */ + public static Element getRootElement(Document doc) { + return (null == doc) ? null : doc.getDocumentElement(); + } + + /** + * 去除XML文本中的无效字符 + * + * @param xmlContent XML文本 + * @return 当传入为null时返回null + */ + public static String cleanInvalid(String xmlContent) { + if (xmlContent == null) { + return null; + } + return xmlContent.replaceAll(INVALID_REGEX, ""); + } + + /** + * 根据节点名获得子节点列表 + * + * @param element 节点 + * @param tagName 节点名,如果节点名为空(null或blank),返回所有子节点 + * @return 节点列表 + */ + public static List getElements(Element element, String tagName) { + final NodeList nodeList = StrUtil.isBlank(tagName) ? element.getChildNodes() : element.getElementsByTagName(tagName); + return transElements(element, nodeList); + } + + /** + * 根据节点名获得第一个子节点 + * + * @param element 节点 + * @param tagName 节点名 + * @return 节点 + */ + public static Element getElement(Element element, String tagName) { + final NodeList nodeList = element.getElementsByTagName(tagName); + if (nodeList == null || nodeList.getLength() < 1) { + return null; + } + int length = nodeList.getLength(); + for (int i = 0; i < length; i++) { + Element childEle = (Element) nodeList.item(i); + if (childEle == null || childEle.getParentNode() == element) { + return childEle; + } + } + return null; + } + + /** + * 根据节点名获得第一个子节点 + * + * @param element 节点 + * @param tagName 节点名 + * @return 节点中的值 + */ + public static String elementText(Element element, String tagName) { + Element child = getElement(element, tagName); + return child == null ? null : child.getTextContent(); + } + + /** + * 根据节点名获得第一个子节点 + * + * @param element 节点 + * @param tagName 节点名 + * @param defaultValue 默认值 + * @return 节点中的值 + */ + public static String elementText(Element element, String tagName, String defaultValue) { + Element child = getElement(element, tagName); + return child == null ? defaultValue : child.getTextContent(); + } + + /** + * 将NodeList转换为Element列表 + * + * @param nodeList NodeList + * @return Element列表 + */ + public static List transElements(NodeList nodeList) { + return transElements(null, nodeList); + } + + /** + * 将NodeList转换为Element列表
+ * 非Element节点将被忽略 + * + * @param parentEle 父节点,如果指定将返回此节点的所有直接子节点,nul返回所有就节点 + * @param nodeList NodeList + * @return Element列表 + */ + public static List transElements(Element parentEle, NodeList nodeList) { + int length = nodeList.getLength(); + final ArrayList elements = new ArrayList(length); + Node node; + Element element; + for (int i = 0; i < length; i++) { + node = nodeList.item(i); + if (Node.ELEMENT_NODE == node.getNodeType()) { + element = (Element) nodeList.item(i); + if (parentEle == null || element.getParentNode() == parentEle) { + elements.add(element); + } + } + } + + return elements; + } + + /** + * 将可序列化的对象转换为XML写入文件,已经存在的文件将被覆盖
+ * Writes serializable object to a XML file. Existing file will be overwritten + * + * @param dest 目标文件 + * @param bean 对象 + * @throws IOException IO异常 + */ + public static void writeObjectAsXml(File dest, Object bean) throws IOException { + XMLEncoder xmlenc = null; + try { + xmlenc = new XMLEncoder(FileUtil.getOutputStream(dest)); + xmlenc.writeObject(bean); + } finally { + // 关闭XMLEncoder会相应关闭OutputStream + IoUtil.close(xmlenc); + } + } + + /** + * 创建XPath
+ * Xpath相关文章:https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html + * + * @return {@link XPath} + * @since 3.2.0 + */ + public static XPath createXPath() { + return XPathFactory.newInstance().newXPath(); + } + + /** + * 通过XPath方式读取XML节点等信息
+ * Xpath相关文章:https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html + * + * @param expression XPath表达式 + * @param source 资源,可以是Docunent、Node节点等 + * @return 匹配返回类型的值 + * @since 4.0.9 + */ + public static Element getElementByXPath(String expression, Object source) { + return (Element) getNodeByXPath(expression, source); + } + + /** + * 通过XPath方式读取XML的NodeList
+ * Xpath相关文章:https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html + * + * @param expression XPath表达式 + * @param source 资源,可以是Docunent、Node节点等 + * @return NodeList + * @since 4.0.9 + */ + public static NodeList getNodeListByXPath(String expression, Object source) { + return (NodeList) getByXPath(expression, source, XPathConstants.NODESET); + } + + /** + * 通过XPath方式读取XML节点等信息
+ * Xpath相关文章:https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html + * + * @param expression XPath表达式 + * @param source 资源,可以是Docunent、Node节点等 + * @return 匹配返回类型的值 + * @since 4.0.9 + */ + public static Node getNodeByXPath(String expression, Object source) { + return (Node) getByXPath(expression, source, XPathConstants.NODE); + } + + /** + * 通过XPath方式读取XML节点等信息
+ * Xpath相关文章:https://www.ibm.com/developerworks/cn/xml/x-javaxpathapi.html + * + * @param expression XPath表达式 + * @param source 资源,可以是Docunent、Node节点等 + * @param returnType 返回类型,{@link javax.xml.xpath.XPathConstants} + * @return 匹配返回类型的值 + * @since 3.2.0 + */ + public static Object getByXPath(String expression, Object source, QName returnType) { + final XPath xPath = createXPath(); + try { + if (source instanceof InputSource) { + return xPath.evaluate(expression, (InputSource) source, returnType); + } else { + return xPath.evaluate(expression, source, returnType); + } + } catch (XPathExpressionException e) { + throw new UtilException(e); + } + } + + /** + * 转义XML特殊字符: + * + *
+	 * & (ampersand) 替换为 &amp;
+	 * < (小于) 替换为 &lt;
+	 * > (大于) 替换为 &gt;
+	 * " (双引号) 替换为 &quot;
+	 * 
+ * + * @param string 被替换的字符串 + * @return 替换后的字符串 + * @since 4.0.8 + */ + public static String escape(String string) { + final StringBuilder sb = new StringBuilder(string.length()); + for (int i = 0, length = string.length(); i < length; i++) { + char c = string.charAt(i); + switch (c) { + case '&': + sb.append("&"); + break; + case '<': + sb.append("<"); + break; + case '>': + sb.append(">"); + break; + case '"': + sb.append("""); + break; + case '\'': + sb.append("'"); + break; + default: + sb.append(c); + } + } + return sb.toString(); + } + + /** + * XML格式字符串转换为Map + * + * @param xmlStr XML字符串 + * @return XML数据转换后的Map + * @since 4.0.8 + */ + public static Map xmlToMap(String xmlStr) { + return xmlToMap(xmlStr, new HashMap()); + } + + /** + * XML格式字符串转换为Map + * + * @param node XML节点 + * @return XML数据转换后的Map + * @since 4.0.8 + */ + public static Map xmlToMap(Node node) { + return xmlToMap(node, new HashMap()); + } + + /** + * XML格式字符串转换为Map
+ * 只支持第一级别的XML,不支持多级XML + * + * @param xmlStr XML字符串 + * @param result 结果Map类型 + * @return XML数据转换后的Map + * @since 4.0.8 + */ + public static Map xmlToMap(String xmlStr, Map result) { + final Document doc = parseXml(xmlStr); + final Element root = getRootElement(doc); + root.normalize(); + + return xmlToMap(root, result); + } + + /** + * XML节点转换为Map + * + * @param node XML节点 + * @param result 结果Map类型 + * @return XML数据转换后的Map + * @since 4.0.8 + */ + public static Map xmlToMap(Node node, Map result) { + if (null == result) { + result = new HashMap<>(); + } + + final NodeList nodeList = node.getChildNodes(); + final int length = nodeList.getLength(); + Node childNode; + Element childEle; + for (int i = 0; i < length; ++i) { + childNode = nodeList.item(i); + if (isElement(childNode)) { + childEle = (Element) childNode; + result.put(childEle.getNodeName(), childEle.getTextContent()); + } + } + return result; + } + + /** + * 将Map转换为XML格式的字符串 + * + * @param data Map类型数据 + * @return XML格式的字符串 + * @since 4.0.8 + */ + public static String mapToXmlStr(Map data, String rootName) { + return toStr(mapToXml(data, rootName)); + } + + /** + * 将Map转换为XML + * + * @param data Map类型数据 + * @return XML + * @since 4.0.9 + */ + public static Document mapToXml(Map data, String rootName) { + final Document doc = createXml(); + final Element root = appendChild(doc, rootName); + + mapToXml(doc, root, data); + return doc; + } + + /** + * 给定节点是否为{@link Element} 类型节点 + * + * @param node 节点 + * @return 是否为{@link Element} 类型节点 + * @since 4.0.8 + */ + public static boolean isElement(Node node) { + return (null == node) ? false : Node.ELEMENT_NODE == node.getNodeType(); + } + + /** + * 在已有节点上创建子节点 + * + * @param node 节点 + * @param tagName 标签名 + * @return 子节点 + * @since 4.0.9 + */ + public static Element appendChild(Node node, String tagName) { + Document doc = (node instanceof Document) ? (Document) node : node.getOwnerDocument(); + Element child = doc.createElement(tagName); + node.appendChild(child); + return child; + } + + // ---------------------------------------------------------------------------------------- Private method start + /** + * 将Map转换为XML格式的字符串 + * + * @param doc {@link Document} + * @param element 节点 + * @param data Map类型数据 + * @since 4.0.8 + */ + private static void mapToXml(Document doc, Element element, Map data) { + Element filedEle; + Object key; + for (Entry entry : data.entrySet()) { + key = entry.getKey(); + if (null != key) { + // key作为标签名 + filedEle = doc.createElement(key.toString()); + element.appendChild(filedEle); + final Object value = entry.getValue(); + // value作为标签内的值。 + if (null != value) { + if (value instanceof Map) { + // 如果值依旧为map,递归继续 + mapToXml(doc, filedEle, (Map) value); + element.appendChild(filedEle); + } else { + filedEle.appendChild(doc.createTextNode(value.toString())); + } + } + } + } + } + + /** + * 关闭XXE,避免漏洞攻击
+ * see: https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Prevention_Cheat_Sheet#JAXP_DocumentBuilderFactory.2C_SAXParserFactory_and_DOM4J + * + * @param dbf DocumentBuilderFactory + * @return DocumentBuilderFactory + */ + private static DocumentBuilderFactory disableXXE(DocumentBuilderFactory dbf) { + String feature; + try { + // This is the PRIMARY defense. If DTDs (doctypes) are disallowed, almost all XML entity attacks are prevented + // Xerces 2 only - http://xerces.apache.org/xerces2-j/features.html#disallow-doctype-decl + feature = "http://apache.org/xml/features/disallow-doctype-decl"; + dbf.setFeature(feature, true); + // If you can't completely disable DTDs, then at least do the following: + // Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-general-entities + // Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-general-entities + // JDK7+ - http://xml.org/sax/features/external-general-entities + feature = "http://xml.org/sax/features/external-general-entities"; + dbf.setFeature(feature, false); + // Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-parameter-entities + // Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-parameter-entities + // JDK7+ - http://xml.org/sax/features/external-parameter-entities + feature = "http://xml.org/sax/features/external-parameter-entities"; + dbf.setFeature(feature, false); + // Disable external DTDs as well + feature = "http://apache.org/xml/features/nonvalidating/load-external-dtd"; + dbf.setFeature(feature, false); + // and these as well, per Timothy Morgan's 2014 paper: "XML Schema, DTD, and Entity Attacks" + dbf.setXIncludeAware(false); + dbf.setExpandEntityReferences(false); + } catch (ParserConfigurationException e) { + // ignore + } + return dbf; + } + // ---------------------------------------------------------------------------------------- Private method end + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/util/ZipUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/ZipUtil.java new file mode 100644 index 000000000..5fd6379ec --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/util/ZipUtil.java @@ -0,0 +1,1019 @@ +package cn.hutool.core.util; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.Enumeration; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; +import java.util.zip.Inflater; +import java.util.zip.InflaterOutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +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; + +/** + * 压缩工具类 + * + * @author Looly + * + */ +public class ZipUtil { + + private static final int DEFAULT_BYTE_ARRAY_LENGTH = 32; + + /** 默认编码,使用平台相关编码 */ + private static final Charset DEFAULT_CHARSET = CharsetUtil.defaultCharset(); + + /** + * 打包到当前目录,使用默认编码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 { + validateFiles(zipFile, srcFiles); + + try (ZipOutputStream out = getZipOutputStream(zipFile, charset)) { + String srcRootDir; + for (File srcFile : srcFiles) { + if (null == srcFile) { + continue; + } + // 如果只是压缩一个文件,则需要截取该文件的父目录 + srcRootDir = srcFile.getCanonicalPath(); + if (srcFile.isFile() || withSrcDir) { + // 若是文件,则将父目录完整路径都截取掉;若设置包含目录,则将上级目录全部截取掉,保留本目录名 + srcRootDir = srcFile.getCanonicalFile().getParentFile().getCanonicalPath(); + } + // 调用递归压缩方法进行目录或文件压缩 + zip(srcFile, srcRootDir, out); + out.flush(); + } + } catch (IOException e) { + throw new UtilException(e); + } + return zipFile; + } + + /** + * 对流中的数据加入到压缩文件,使用默认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 { + 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 !"); + } + + ZipOutputStream out = null; + try { + out = getZipOutputStream(zipFile, charset); + for (int i = 0; i < paths.length; i++) { + addFile(ins[i], paths[i], out); + } + } finally { + IoUtil.close(out); + } + 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 { + return unzip(zipFile, FileUtil.file(zipFile.getParentFile(), FileUtil.mainName(zipFile)), 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 解压的目录 + * @throws UtilException IO异常 + * @since 3.2.2 + */ + public static File unzip(File zipFile, File outFile, Charset charset) throws UtilException { + ZipFile zip; + try { + zip = new ZipFile(zipFile, charset); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return unzip(zip, outFile); + } + + /** + * 解压 + * + * @param zipFile zip文件,附带编码信息,使用完毕自动关闭 + * @param outFile 解压到的目录 + * @return 解压的目录 + * @throws IORuntimeException IO异常 + * @since 4.5.8 + */ + @SuppressWarnings("unchecked") + public static File unzip(ZipFile zipFile, File outFile) throws IORuntimeException { + try { + final Enumeration em = (Enumeration) zipFile.entries(); + ZipEntry zipEntry = null; + File outItemFile = null; + while (em.hasMoreElements()) { + zipEntry = em.nextElement(); + // FileUtil.file会检查slip漏洞,漏洞说明见http://blog.nsfocus.net/zip-slip-2/ + outItemFile = FileUtil.file(outFile, zipEntry.getName()); + if (zipEntry.isDirectory()) { + // 创建对应目录 + outItemFile.mkdirs(); + } else { + // 写出文件 + write(zipFile, zipEntry, outItemFile); + } + } + } finally { + IoUtil.close(zipFile); + } + return outFile; + } + + /** + * 解压
+ * 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 { + ZipEntry zipEntry = null; + File outItemFile = null; + while (null != (zipEntry = zipStream.getNextEntry())) { + // FileUtil.file会检查slip漏洞,漏洞说明见http://blog.nsfocus.net/zip-slip-2/ + outItemFile = FileUtil.file(outFile, zipEntry.getName()); + if (zipEntry.isDirectory()) { + // 目录 + outItemFile.mkdirs(); + } else { + // 文件 + FileUtil.writeFromStream(zipStream, outItemFile); + } + } + } catch (IOException e) { + throw new UtilException(e); + } finally { + IoUtil.close(zipStream); + } + return outFile; + } + + /** + * 从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 + */ + @SuppressWarnings("unchecked") + public static byte[] unzipFileBytes(File zipFile, Charset charset, String name) { + ZipFile zipFileObj = null; + try { + zipFileObj = new ZipFile(zipFile, charset); + final Enumeration em = (Enumeration) zipFileObj.entries(); + ZipEntry zipEntry = null; + while (em.hasMoreElements()) { + zipEntry = em.nextElement(); + if (zipEntry.isDirectory()) { + continue; + } else if (name.equals(zipEntry.getName())) { + return IoUtil.readBytes(zipFileObj.getInputStream(zipEntry)); + } + } + } catch (IOException e) { + throw new UtilException(e); + } finally { + IoUtil.close(zipFileObj); + } + return null; + } + + // ----------------------------------------------------------------------------- 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); + GZIPOutputStream gos = null; + try { + gos = new GZIPOutputStream(bos); + IoUtil.copy(in, gos); + } catch (IOException e) { + throw new UtilException(e); + } finally { + IoUtil.close(gos); + } + // 返回必须在关闭gos后进行,因为关闭时会自动执行finish()方法,保证数据全部写出 + 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 { + GZIPInputStream gzi = null; + FastByteArrayOutputStream bos = null; + try { + gzi = (in instanceof GZIPInputStream) ? (GZIPInputStream) in : new GZIPInputStream(in); + bos = new FastByteArrayOutputStream(length); + IoUtil.copy(gzi, bos); + } catch (IOException e) { + throw new UtilException(e); + } finally { + IoUtil.close(gzi); + } + // 返回必须在关闭gos后进行,因为关闭时会自动执行finish()方法,保证数据全部写出 + 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); + deflater(in, out, level, false); + 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); + inflater(in, out, false); + return out.toByteArray(); + } + + // ---------------------------------------------------------------------------------------------- Private method start + /** + * 获得 {@link ZipOutputStream} + * + * @param zipFile 压缩文件 + * @param charset 编码 + * @return {@link ZipOutputStream} + */ + private static ZipOutputStream getZipOutputStream(File zipFile, Charset charset) { + return getZipOutputStream(FileUtil.getOutputStream(zipFile), charset); + } + + /** + * 获得 {@link ZipOutputStream} + * + * @param zipFile 压缩文件 + * @param charset 编码 + * @return {@link ZipOutputStream} + */ + private static ZipOutputStream getZipOutputStream(OutputStream out, Charset charset) { + charset = (null == charset) ? DEFAULT_CHARSET : charset; + return new ZipOutputStream(out, charset); + } + + /** + * 递归压缩文件夹
+ * srcRootDir决定了路径截取的位置,例如:
+ * file的路径为d:/a/b/c/d.txt,srcRootDir为d:/a/b,则压缩后的文件与目录为结构为c/d.txt + * + * @param out 压缩文件存储对象 + * @param srcRootDir 被压缩的文件夹根目录 + * @param file 当前递归压缩的文件或目录对象 + * @throws UtilException IO异常 + */ + private static void zip(File file, String srcRootDir, ZipOutputStream out) throws UtilException { + if (file == null) { + return; + } + + final String subPath = FileUtil.subPath(srcRootDir, file); // 获取文件相对于压缩文件夹根目录的子路径 + if (file.isDirectory()) {// 如果是目录,则压缩压缩目录中的文件或子目录 + final File[] files = file.listFiles(); + if (ArrayUtil.isEmpty(files) && StrUtil.isNotEmpty(subPath)) { + // 加入目录,只有空目录时才加入目录,非空时会在创建文件时自动添加父级目录 + addDir(subPath, out); + } + // 压缩目录下的子文件或目录 + for (File childFile : files) { + zip(childFile, srcRootDir, out); + } + } else {// 如果是文件或其它符号,则直接压缩该文件 + addFile(file, subPath, out); + } + } + + /** + * 添加文件到压缩包 + * + * @param file 需要压缩的文件 + * @param path 在压缩文件中的路径 + * @param out 压缩文件存储对象 + * @throws UtilException IO异常 + * @since 4.0.5 + */ + private static void addFile(File file, String path, ZipOutputStream out) throws UtilException { + BufferedInputStream in = null; + try { + in = FileUtil.getInputStream(file); + addFile(in, path, out); + } finally { + IoUtil.close(in); + } + } + + /** + * 添加文件流到压缩包,不关闭输入流 + * + * @param in 需要压缩的输入流 + * @param path 压缩的路径 + * @param out 压缩文件存储对象 + * @throws UtilException IO异常 + */ + private static void addFile(InputStream in, String path, ZipOutputStream out) throws UtilException { + if (null == in) { + return; + } + try { + out.putNextEntry(new ZipEntry(path)); + IoUtil.copy(in, out); + } catch (IOException e) { + throw new UtilException(e); + } finally { + closeEntry(out); + } + } + + /** + * 在压缩包中新建目录 + * + * @param path 压缩的路径 + * @param out 压缩文件存储对象 + * @throws UtilException IO异常 + */ + private static void addDir(String path, ZipOutputStream out) throws UtilException { + path = StrUtil.addSuffixIfNot(path, StrUtil.SLASH); + try { + out.putNextEntry(new ZipEntry(path)); + } catch (IOException e) { + throw new UtilException(e); + } finally { + closeEntry(out); + } + } + + /** + * 判断压缩文件保存的路径是否为源文件路径的子文件夹,如果是,则抛出异常(防止无限递归压缩的发生) + * + * @param zipFile 压缩后的产生的文件路径 + * @param srcFile 被压缩的文件或目录 + */ + 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 (false == srcFile.exists()) { + throw new UtilException(StrUtil.format("File [{}] not exist!", srcFile.getAbsolutePath())); + } + + try { + final File parentFile = zipFile.getCanonicalFile().getParentFile(); + // 压缩文件不能位于被压缩的目录内 + if (srcFile.isDirectory() && parentFile.getCanonicalPath().contains(srcFile.getCanonicalPath())) { + throw new UtilException("Zip file path [{}] must not be the child directory of [{}] !", zipFile.getCanonicalPath(), srcFile.getCanonicalPath()); + } + + } catch (IOException e) { + throw new UtilException(e); + } + } + } + + /** + * 关闭当前Entry,继续下一个Entry + * + * @param out ZipOutputStream + */ + private static void closeEntry(ZipOutputStream out) { + try { + out.closeEntry(); + } catch (IOException e) { + // ignore + } + } + + /** + * 从Zip中读取文件流并写出到文件 + * + * @param zipFile Zip文件 + * @param zipEntry zip文件中的子文件 + * @param outItemFile 输出到的文件 + * @throws IORuntimeException IO异常 + */ + private static void write(ZipFile zipFile, ZipEntry zipEntry, File outItemFile) throws IORuntimeException { + InputStream in = null; + try { + in = zipFile.getInputStream(zipEntry); + FileUtil.writeFromStream(in, outItemFile); + } catch (IOException e) { + throw new IORuntimeException(e); + } finally { + IoUtil.close(in); + } + } + + /** + * 将Zlib流解压到out中 + * + * @param in zlib数据流 + * @param out 输出 + * @param nowrap true表示兼容Gzip压缩 + */ + private static void inflater(InputStream in, OutputStream out, boolean nowrap) { + final InflaterOutputStream ios = (out instanceof InflaterOutputStream) ? (InflaterOutputStream) out : new InflaterOutputStream(out, new Inflater(nowrap)); + IoUtil.copy(in, ios); + try { + ios.finish(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 将普通数据流压缩成zlib到out中 + * + * @param in zlib数据流 + * @param out 输出 + * @param level 压缩级别,0~9 + * @param nowrap true表示兼容Gzip压缩 + */ + private static void deflater(InputStream in, OutputStream out, int level, boolean nowrap) { + final DeflaterOutputStream ios = (out instanceof DeflaterOutputStream) ? (DeflaterOutputStream) out : new DeflaterOutputStream(out, new Deflater(level, nowrap)); + IoUtil.copy(in, ios); + try { + ios.finish(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + // ---------------------------------------------------------------------------------------------- Private method end + +} \ No newline at end of file diff --git a/hutool-core/src/main/java/cn/hutool/core/util/package-info.java b/hutool-core/src/main/java/cn/hutool/core/util/package-info.java new file mode 100644 index 000000000..965eb9b83 --- /dev/null +++ b/hutool-core/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/hutool-core/src/test/java/cn/hutool/core/annotation/AnnotationForTest.java b/hutool-core/src/test/java/cn/hutool/core/annotation/AnnotationForTest.java new file mode 100644 index 000000000..10d99596a --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/annotation/AnnotationForTest.java @@ -0,0 +1,27 @@ +package cn.hutool.core.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 用于单元测试的注解类
+ * 注解类相关说明见:https://www.cnblogs.com/xdp-gacl/p/3622275.html + * + * @author looly + * + */ +// Retention注解决定MyAnnotation注解的生命周期 +@Retention(RetentionPolicy.RUNTIME) +// Target注解决定MyAnnotation注解可以加在哪些成分上,如加在类身上,或者属性身上,或者方法身上等成分 +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface AnnotationForTest { + + /** + * 注解的默认属性值 + * + * @return + */ + String value(); +} diff --git a/hutool-core/src/test/java/cn/hutool/core/annotation/AnnotationUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/annotation/AnnotationUtilTest.java new file mode 100644 index 000000000..64f3d6342 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/annotation/AnnotationUtilTest.java @@ -0,0 +1,18 @@ +package cn.hutool.core.annotation; + +import org.junit.Assert; +import org.junit.Test; + +public class AnnotationUtilTest { + + @Test + public void getAnnotationValueTest() { + Object value = AnnotationUtil.getAnnotationValue(ClassWithAnnotation.class, AnnotationForTest.class); + Assert.assertEquals("测试", value); + } + + @AnnotationForTest("测试") + class ClassWithAnnotation{ + + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/bean/BeanDescTest.java b/hutool-core/src/test/java/cn/hutool/core/bean/BeanDescTest.java new file mode 100644 index 000000000..5139c758b --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/bean/BeanDescTest.java @@ -0,0 +1,128 @@ +package cn.hutool.core.bean; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.bean.BeanDesc.PropDesc; + +/** + * {@link BeanDesc} 单元测试类 + * + * @author looly + * + */ +public class BeanDescTest { + + @Test + public void propDescTes() { + BeanDesc desc = BeanUtil.getBeanDesc(User.class); + Assert.assertEquals("User", desc.getSimpleName()); + + Assert.assertEquals("age", desc.getField("age").getName()); + Assert.assertEquals("getAge", desc.getGetter("age").getName()); + Assert.assertEquals("setAge", desc.getSetter("age").getName()); + Assert.assertEquals(1, desc.getSetter("age").getParameterTypes().length); + Assert.assertEquals(int.class, desc.getSetter("age").getParameterTypes()[0]); + + } + + @Test + public void propDescTes2() { + BeanDesc desc = BeanUtil.getBeanDesc(User.class); + + PropDesc prop = desc.getProp("name"); + Assert.assertEquals("name", prop.getFieldName()); + Assert.assertEquals("getName", prop.getGetter().getName()); + Assert.assertEquals("setName", prop.getSetter().getName()); + Assert.assertEquals(1, prop.getSetter().getParameterTypes().length); + Assert.assertEquals(String.class, prop.getSetter().getParameterTypes()[0]); + } + + @Test + public void propDescOfBooleanTest() { + BeanDesc desc = BeanUtil.getBeanDesc(User.class); + + Assert.assertEquals("isAdmin", desc.getGetter("isAdmin").getName()); + Assert.assertEquals("setAdmin", desc.getSetter("isAdmin").getName()); + Assert.assertEquals("isGender", desc.getGetter("gender").getName()); + Assert.assertEquals("setGender", desc.getSetter("gender").getName()); + } + + @Test + public void propDescOfBooleanTest2() { + BeanDesc desc = BeanUtil.getBeanDesc(User.class); + + Assert.assertEquals("isIsSuper", desc.getGetter("isSuper").getName()); + Assert.assertEquals("setIsSuper", desc.getSetter("isSuper").getName()); + } + + @Test + public void getSetTest() { + BeanDesc desc = BeanUtil.getBeanDesc(User.class); + + User user = new User(); + desc.getProp("name").setValue(user, "张三"); + Assert.assertEquals("张三", user.getName()); + + Object value = desc.getProp("name").getValue(user); + Assert.assertEquals("张三", value); + } + + public static class User { + private String name; + private int age; + private boolean isAdmin; + private boolean isSuper; + private boolean gender; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public User setAge(int age) { + this.age = age; + return this; + } + + public String testMethod() { + return "test for " + this.name; + } + + public boolean isAdmin() { + return isAdmin; + } + + public void setAdmin(boolean isAdmin) { + this.isAdmin = isAdmin; + } + + public boolean isIsSuper() { + return isSuper; + } + + public void setIsSuper(boolean isSuper) { + this.isSuper = isSuper; + } + + public boolean isGender() { + return gender; + } + + public void setGender(boolean gender) { + this.gender = gender; + } + + @Override + public String toString() { + return "User [name=" + name + ", age=" + age + ", isAdmin=" + isAdmin + ", gender=" + gender + "]"; + } + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/bean/BeanPathTest.java b/hutool-core/src/test/java/cn/hutool/core/bean/BeanPathTest.java new file mode 100644 index 000000000..3e1a6f8f5 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/bean/BeanPathTest.java @@ -0,0 +1,101 @@ +package cn.hutool.core.bean; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import cn.hutool.core.lang.test.bean.ExamInfoDict; +import cn.hutool.core.lang.test.bean.UserInfoDict; + +/** + * {@link BeanPath} 单元测试 + * + * @author looly + * + */ +public class BeanPathTest { + + Map tempMap; + + @Before + public void init() { + // ------------------------------------------------- 考试信息列表 + ExamInfoDict examInfoDict = new ExamInfoDict(); + examInfoDict.setId(1); + examInfoDict.setExamType(0); + examInfoDict.setAnswerIs(1); + + ExamInfoDict examInfoDict1 = new ExamInfoDict(); + examInfoDict1.setId(2); + examInfoDict1.setExamType(0); + examInfoDict1.setAnswerIs(0); + + ExamInfoDict examInfoDict2 = new ExamInfoDict(); + examInfoDict2.setId(3); + examInfoDict2.setExamType(1); + examInfoDict2.setAnswerIs(0); + + List examInfoDicts = new ArrayList(); + examInfoDicts.add(examInfoDict); + examInfoDicts.add(examInfoDict1); + examInfoDicts.add(examInfoDict2); + + // ------------------------------------------------- 用户信息 + UserInfoDict userInfoDict = new UserInfoDict(); + userInfoDict.setId(1); + userInfoDict.setPhotoPath("yx.mm.com"); + userInfoDict.setRealName("张三"); + userInfoDict.setExamInfoDict(examInfoDicts); + + tempMap = new HashMap(); + tempMap.put("userInfo", userInfoDict); + tempMap.put("flag", 1); + } + + @Test + public void beanPathTest1() { + BeanPath pattern = new BeanPath("userInfo.examInfoDict[0].id"); + Assert.assertEquals("userInfo", pattern.patternParts.get(0)); + Assert.assertEquals("examInfoDict", pattern.patternParts.get(1)); + Assert.assertEquals("0", pattern.patternParts.get(2)); + Assert.assertEquals("id", pattern.patternParts.get(3)); + } + + @Test + public void beanPathTest2() { + BeanPath pattern = new BeanPath("[userInfo][examInfoDict][0][id]"); + Assert.assertEquals("userInfo", pattern.patternParts.get(0)); + Assert.assertEquals("examInfoDict", pattern.patternParts.get(1)); + Assert.assertEquals("0", pattern.patternParts.get(2)); + Assert.assertEquals("id", pattern.patternParts.get(3)); + } + + @Test + public void beanPathTest3() { + BeanPath pattern = new BeanPath("['userInfo']['examInfoDict'][0]['id']"); + Assert.assertEquals("userInfo", pattern.patternParts.get(0)); + Assert.assertEquals("examInfoDict", pattern.patternParts.get(1)); + Assert.assertEquals("0", pattern.patternParts.get(2)); + Assert.assertEquals("id", pattern.patternParts.get(3)); + } + + @Test + public void getTest() { + BeanPath pattern = BeanPath.create("userInfo.examInfoDict[0].id"); + Object result = pattern.get(tempMap); + Assert.assertEquals(1, result); + } + + @Test + public void setTest() { + BeanPath pattern = BeanPath.create("userInfo.examInfoDict[0].id"); + pattern.set(tempMap, 2); + Object result = pattern.get(tempMap); + Assert.assertEquals(2, result); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/bean/BeanUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/bean/BeanUtilTest.java new file mode 100644 index 000000000..ce28d3f60 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/bean/BeanUtilTest.java @@ -0,0 +1,303 @@ +package cn.hutool.core.bean; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.UUID; + +import org.junit.Assert; +import org.junit.Test; + +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.CollectionUtil; +import cn.hutool.core.map.MapUtil; + +/** + * Bean工具单元测试 + * + * @author Looly + * + */ +public class BeanUtilTest { + + @Test + public void isBeanTest() { + + // HashMap不包含setXXX方法,不是bean + boolean isBean = BeanUtil.isBean(HashMap.class); + Assert.assertFalse(isBean); + } + + @Test + public void fillBeanTest() { + Person person = BeanUtil.fillBean(new Person(), new ValueProvider() { + + @Override + public Object value(String key, Type valueType) { + switch (key) { + case "name": + return "张三"; + case "age": + return 18; + } + return null; + } + + @Override + public boolean containsKey(String key) { + // 总是存在key + return true; + } + + }, CopyOptions.create()); + + Assert.assertEquals(person.getName(), "张三"); + Assert.assertEquals(person.getAge(), 18); + } + + @Test + public void fillBeanWithMapIgnoreCaseTest() { + HashMap map = CollectionUtil.newHashMap(); + map.put("Name", "Joe"); + map.put("aGe", 12); + map.put("openId", "DFDFSDFWERWER"); + SubPerson person = BeanUtil.fillBeanWithMapIgnoreCase(map, new SubPerson(), false); + Assert.assertEquals(person.getName(), "Joe"); + Assert.assertEquals(person.getAge(), 12); + Assert.assertEquals(person.getOpenid(), "DFDFSDFWERWER"); + } + + @Test + public void mapToBeanIgnoreCaseTest() { + HashMap map = CollectionUtil.newHashMap(); + map.put("Name", "Joe"); + map.put("aGe", 12); + + Person person = BeanUtil.mapToBeanIgnoreCase(map, Person.class, false); + Assert.assertEquals("Joe", person.getName()); + Assert.assertEquals(12, person.getAge()); + } + + @Test + public void mapToBeanTest() { + HashMap map = CollectionUtil.newHashMap(); + map.put("a_name", "Joe"); + map.put("b_age", 12); + + // 别名 + HashMap mapping = CollUtil.newHashMap(); + mapping.put("a_name", "name"); + mapping.put("b_age", "age"); + + Person person = BeanUtil.mapToBean(map, Person.class, CopyOptions.create().setFieldMapping(mapping)); + Assert.assertEquals("Joe", person.getName()); + Assert.assertEquals(12, person.getAge()); + } + + @Test + public void beanToMapTest() { + SubPerson person = new SubPerson(); + person.setAge(14); + person.setOpenid("11213232"); + person.setName("测试A11"); + person.setSubName("sub名字"); + + Map map = BeanUtil.beanToMap(person); + + Assert.assertEquals("测试A11", map.get("name")); + Assert.assertEquals(14, map.get("age")); + Assert.assertEquals("11213232", map.get("openid")); + // static属性应被忽略 + Assert.assertFalse(map.containsKey("SUBNAME")); + } + + @Test + public void beanToMapTest2() { + SubPerson person = new SubPerson(); + person.setAge(14); + person.setOpenid("11213232"); + person.setName("测试A11"); + person.setSubName("sub名字"); + + Map map = BeanUtil.beanToMap(person, true, true); + Assert.assertEquals("sub名字", map.get("sub_name")); + } + + @Test + public void getPropertyTest() { + SubPerson person = new SubPerson(); + person.setAge(14); + person.setOpenid("11213232"); + person.setName("测试A11"); + person.setSubName("sub名字"); + + Object name = BeanUtil.getProperty(person, "name"); + Assert.assertEquals("测试A11", name); + Object subName = BeanUtil.getProperty(person, "subName"); + Assert.assertEquals("sub名字", subName); + } + + @Test + public void getPropertyDescriptorsTest() { + HashSet set = CollUtil.newHashSet(); + PropertyDescriptor[] propertyDescriptors = BeanUtil.getPropertyDescriptors(SubPerson.class); + for (PropertyDescriptor propertyDescriptor : propertyDescriptors) { + set.add(propertyDescriptor.getName()); + } + Assert.assertTrue(set.contains("age")); + Assert.assertTrue(set.contains("id")); + Assert.assertTrue(set.contains("name")); + Assert.assertTrue(set.contains("openid")); + Assert.assertTrue(set.contains("slow")); + Assert.assertTrue(set.contains("subName")); + } + + @Test + public void copyPropertiesHasBooleanTest() { + SubPerson p1 = new SubPerson(); + p1.setSlow(true); + + // 测试boolean参数值isXXX形式 + SubPerson p2 = new SubPerson(); + BeanUtil.copyProperties(p1, p2); + Assert.assertTrue(p2.isSlow()); + + // 测试boolean参数值非isXXX形式 + SubPerson2 p3 = new SubPerson2(); + BeanUtil.copyProperties(p1, p3); + Assert.assertTrue(p3.isSlow()); + } + + @Test + public void copyPropertiesBeanToMapTest() { + // 测试BeanToMap + SubPerson p1 = new SubPerson(); + p1.setSlow(true); + p1.setName("测试"); + p1.setSubName("sub测试"); + + Map map = MapUtil.newHashMap(); + BeanUtil.copyProperties(p1, map); + Assert.assertTrue((Boolean) map.get("isSlow")); + Assert.assertEquals("测试", map.get("name")); + Assert.assertEquals("sub测试", map.get("subName")); + } + + @Test + public void copyPropertiesMapToMapTest() { + // 测试MapToMap + Map p1 = new HashMap<>(); + p1.put("isSlow", true); + p1.put("name", "测试"); + p1.put("subName", "sub测试"); + + Map map = MapUtil.newHashMap(); + BeanUtil.copyProperties(p1, map); + Assert.assertTrue((Boolean) map.get("isSlow")); + Assert.assertEquals("测试", map.get("name")); + Assert.assertEquals("sub测试", map.get("subName")); + } + + @Test + public void trimBeanStrFieldsTest() { + Person person = new Person(); + person.setAge(1); + person.setName(" 张三 "); + person.setOpenid(null); + Person person2 = BeanUtil.trimStrFields(person); + + // 是否改变原对象 + Assert.assertEquals("张三", person.getName()); + Assert.assertEquals("张三", person2.getName()); + } + + // ----------------------------------------------------------------------------------------------------------------- + public static class SubPerson extends Person { + + public static final String SUBNAME = "TEST"; + + private UUID id; + private String subName; + private Boolean isSlow; + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getSubName() { + return subName; + } + + public void setSubName(String subName) { + this.subName = subName; + } + + public Boolean isSlow() { + return isSlow; + } + + public void setSlow(Boolean isSlow) { + this.isSlow = isSlow; + } + } + + public static class SubPerson2 extends Person { + private String subName; + // boolean参数值非isXXX形式 + private Boolean slow; + + public String getSubName() { + return subName; + } + + public void setSubName(String subName) { + this.subName = subName; + } + + public Boolean isSlow() { + return slow; + } + + public void setSlow(Boolean isSlow) { + this.slow = isSlow; + } + } + + public static class Person { + private String name; + private int age; + private String openid; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public String getOpenid() { + return openid; + } + + public void setOpenid(String openid) { + this.openid = openid; + } + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/bean/DynaBeanTest.java b/hutool-core/src/test/java/cn/hutool/core/bean/DynaBeanTest.java new file mode 100644 index 000000000..de54d20c7 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/bean/DynaBeanTest.java @@ -0,0 +1,62 @@ +package cn.hutool.core.bean; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.bean.DynaBean; + +/** + * {@link DynaBean}单元测试 + * @author Looly + * + */ +public class DynaBeanTest { + + @Test + public void beanTest(){ + User user = new User(); + DynaBean bean = DynaBean.create(user); + bean.set("name", "李华"); + bean.set("age", 12); + + String name = bean.get("name"); + Assert.assertEquals(user.getName(), name); + int age = bean.get("age"); + Assert.assertEquals(user.getAge(), age); + + //重复包装测试 + DynaBean bean2 = new DynaBean(bean); + User user2 = bean2.getBean(); + Assert.assertEquals(user, user2); + + //执行指定方法 + Object invoke = bean2.invoke("testMethod"); + Assert.assertEquals("test for 李华", invoke); + } + + public static class User{ + private String name; + private int age; + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + public int getAge() { + return age; + } + public void setAge(int age) { + this.age = age; + } + + public String testMethod(){ + return "test for " + this.name; + } + + @Override + public String toString() { + return "User [name=" + name + ", age=" + age + "]"; + } + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/clone/CloneTest.java b/hutool-core/src/test/java/cn/hutool/core/clone/CloneTest.java new file mode 100644 index 000000000..9fa476b25 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/clone/CloneTest.java @@ -0,0 +1,128 @@ +package cn.hutool.core.clone; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.clone.CloneRuntimeException; +import cn.hutool.core.clone.CloneSupport; +import cn.hutool.core.clone.Cloneable; + +/** + * 克隆单元测试 + * @author Looly + * + */ +public class CloneTest { + + @Test + public void cloneTest(){ + + //实现Cloneable接口 + Cat cat = new Cat(); + Cat cat2 = cat.clone(); + Assert.assertEquals(cat, cat2); + + //继承CloneSupport类 + Dog dog = new Dog(); + Dog dog2 = dog.clone(); + Assert.assertEquals(dog, dog2); + } + + //------------------------------------------------------------------------------- private Class for test + /** + * 猫猫类,使用实现Cloneable方式 + * @author Looly + * + */ + private static class Cat implements Cloneable{ + private String name = "miaomiao"; + private int age = 2; + + @Override + public Cat clone() { + try { + return (Cat) super.clone(); + } catch (CloneNotSupportedException e) { + throw new CloneRuntimeException(e); + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + age; + result = prime * result + ((name == null) ? 0 : name.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; + } + Cat other = (Cat) obj; + if (age != other.age) { + return false; + } + if (name == null) { + if (other.name != null) { + return false; + } + } else if (!name.equals(other.name)) { + return false; + } + return true; + } + } + + /** + * 狗狗类,用于继承CloneSupport类 + * @author Looly + * + */ + private static class Dog extends CloneSupport{ + private String name = "wangwang"; + private int age = 3; + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + age; + result = prime * result + ((name == null) ? 0 : name.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; + } + Dog other = (Dog) obj; + if (age != other.age) { + return false; + } + if (name == null) { + if (other.name != null) { + return false; + } + } else if (!name.equals(other.name)) { + return false; + } + return true; + } + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/codec/BCDTest.java b/hutool-core/src/test/java/cn/hutool/core/codec/BCDTest.java new file mode 100644 index 000000000..67b075050 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/codec/BCDTest.java @@ -0,0 +1,20 @@ +package cn.hutool.core.codec; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.codec.BCD; + +public class BCDTest { + + @Test + public void bcdTest(){ + String strForTest = "123456ABCDEF"; + + //转BCD + byte[] bcd = BCD.strToBcd(strForTest); + String str = BCD.bcdToStr(bcd); + //解码BCD + Assert.assertEquals(strForTest, str); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/codec/Base32Test.java b/hutool-core/src/test/java/cn/hutool/core/codec/Base32Test.java new file mode 100644 index 000000000..fc0f0c5d5 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/codec/Base32Test.java @@ -0,0 +1,19 @@ +package cn.hutool.core.codec; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.codec.Base32; + +public class Base32Test { + + @Test + public void encodeAndDecodeTest(){ + String a = "伦家是一个非常长的字符串"; + String encode = Base32.encode(a); + Assert.assertEquals("4S6KNZNOW3TJRL7EXCAOJOFK5GOZ5ZNYXDUZLP7HTKCOLLMX46WKNZFYWI", encode); + + String decodeStr = Base32.decodeStr(encode); + Assert.assertEquals(a, decodeStr); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/codec/Base62Test.java b/hutool-core/src/test/java/cn/hutool/core/codec/Base62Test.java new file mode 100644 index 000000000..02720cb4d --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/codec/Base62Test.java @@ -0,0 +1,23 @@ +package cn.hutool.core.codec; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Base62单元测试 + * + * @author looly + * + */ +public class Base62Test { + + @Test + public void encodeAndDecodeTest() { + String a = "伦家是一个非常长的字符串66"; + String encode = Base62.encode(a); + Assert.assertEquals("17vKU8W4JMG8dQF8lk9VNnkdMOeWn4rJMva6F0XsLrrT53iKBnqo", encode); + + String decodeStr = Base62.decodeStr(encode); + Assert.assertEquals(a, decodeStr); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/codec/Base64Test.java b/hutool-core/src/test/java/cn/hutool/core/codec/Base64Test.java new file mode 100644 index 000000000..27cf77bcc --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/codec/Base64Test.java @@ -0,0 +1,46 @@ +package cn.hutool.core.codec; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; + +/** + * Base64单元测试 + * + * @author looly + * + */ +public class Base64Test { + + @Test + public void encodeAndDecodeTest() { + String a = "伦家是一个非常长的字符串66"; + String encode = Base64.encode(a); + Assert.assertEquals("5Lym5a625piv5LiA5Liq6Z2e5bi46ZW/55qE5a2X56ym5LiyNjY=", encode); + + String decodeStr = Base64.decodeStr(encode); + Assert.assertEquals(a, decodeStr); + } + + @Test + public void encodeAndDecodeTest2() { + String a = "a61a5db5a67c01445ca2-HZ20181120172058/pdf/中国电信影像云单体网关Docker版-V1.2.pdf"; + String encode = Base64.encode(a, CharsetUtil.UTF_8); + Assert.assertEquals("YTYxYTVkYjVhNjdjMDE0NDVjYTItSFoyMDE4MTEyMDE3MjA1OC9wZGYv5Lit5Zu955S15L+h5b2x5YOP5LqR5Y2V5L2T572R5YWzRG9ja2Vy54mILVYxLjIucGRm", encode); + + String decodeStr = Base64.decodeStr(encode, CharsetUtil.UTF_8); + Assert.assertEquals(a, decodeStr); + } + + @Test + public void urlSafeEncodeAndDecodeTest() { + String a = "广州伦家需要安全感55"; + String encode = StrUtil.utf8Str(Base64.encodeUrlSafe(StrUtil.utf8Bytes(a), false)); + Assert.assertEquals("5bm_5bee5Lym5a626ZyA6KaB5a6J5YWo5oSfNTU", encode); + + String decodeStr = Base64.decodeStr(encode); + Assert.assertEquals(a, decodeStr); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/codec/CaesarTest.java b/hutool-core/src/test/java/cn/hutool/core/codec/CaesarTest.java new file mode 100644 index 000000000..a3145ddf5 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/codec/CaesarTest.java @@ -0,0 +1,18 @@ +package cn.hutool.core.codec; + +import org.junit.Assert; +import org.junit.Test; + +public class CaesarTest { + + @Test + public void caesarTest() { + String str = "1f2e9df6131b480b9fdddc633cf24996"; + + String encode = Caesar.encode(str, 3); + Assert.assertEquals("1H2G9FH6131D480D9HFFFE633EH24996", encode); + + String decode = Caesar.decode(encode, 3); + Assert.assertEquals(str, decode); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/codec/MorseTest.java b/hutool-core/src/test/java/cn/hutool/core/codec/MorseTest.java new file mode 100644 index 000000000..b54be9157 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/codec/MorseTest.java @@ -0,0 +1,33 @@ +package cn.hutool.core.codec; + +import org.junit.Assert; +import org.junit.Test; + +public class MorseTest { + + private final Morse morseCoder = new Morse(); + + @Test + public void test0() { + String text = "Hello World!"; + String morse = "...././.-../.-../---/-...../.--/---/.-./.-../-../-.-.--/"; + Assert.assertEquals(morse, morseCoder.encode(text)); + Assert.assertEquals(morseCoder.decode(morse), text.toUpperCase()); + } + + @Test + public void test1() { + String text = "你好,世界!"; + String morse = "-..----.--...../-.--..-.-----.-/--------....--../-..---....-.--./---.-.-.-..--../--------.......-/"; + Assert.assertEquals(morseCoder.encode(text), morse); + Assert.assertEquals(morseCoder.decode(morse), text); + } + + @Test + public void test2() { + String text = "こんにちは"; + String morse = "--.....-.-..--/--....-..-..--/--.....--.-.--/--.....--....-/--.....--.----/"; + Assert.assertEquals(morseCoder.encode(text), morse); + Assert.assertEquals(morseCoder.decode(morse), text); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/codec/RotTest.java b/hutool-core/src/test/java/cn/hutool/core/codec/RotTest.java new file mode 100644 index 000000000..e90d20e3c --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/codec/RotTest.java @@ -0,0 +1,18 @@ +package cn.hutool.core.codec; + +import org.junit.Assert; +import org.junit.Test; + +public class RotTest { + + @Test + public void rot13Test() { + String str = "1f2e9df6131b480b9fdddc633cf24996"; + + String encode13 = Rot.encode13(str); + Assert.assertEquals("4s5r2qs9464o713o2sqqqp966ps57229", encode13); + + String decode13 = Rot.decode13(encode13); + Assert.assertEquals(str, decode13); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/collection/CollUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/collection/CollUtilTest.java new file mode 100644 index 000000000..8083ab89d --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/collection/CollUtilTest.java @@ -0,0 +1,574 @@ +package cn.hutool.core.collection; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import cn.hutool.core.collection.CollUtil.Hash; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.lang.Dict; +import cn.hutool.core.lang.Editor; +import cn.hutool.core.lang.Matcher; +import cn.hutool.core.map.MapUtil; + +/** + * 集合工具类单元测试 + * + * @author looly + * + */ +public class CollUtilTest { + + @Test + public void newHashSetTest() { + Set set = CollectionUtil.newHashSet((String[]) null); + Assert.assertNotNull(set); + } + + @Test + public void valuesOfKeysTest() { + Dict v1 = Dict.create().set("id", 12).set("name", "张三").set("age", 23); + Dict v2 = Dict.create().set("age", 13).set("id", 15).set("name", "李四"); + + final String[] keys = v1.keySet().toArray(new String[v1.size()]); + ArrayList v1s = CollectionUtil.valuesOfKeys(v1, keys); + Assert.assertTrue(v1s.contains(12)); + Assert.assertTrue(v1s.contains(23)); + Assert.assertTrue(v1s.contains("张三")); + + ArrayList v2s = CollectionUtil.valuesOfKeys(v2, keys); + Assert.assertTrue(v2s.contains(15)); + Assert.assertTrue(v2s.contains(13)); + Assert.assertTrue(v2s.contains("李四")); + } + + @Test + public void unionTest() { + ArrayList list1 = CollectionUtil.newArrayList("a", "b", "b", "c", "d", "x"); + ArrayList list2 = CollectionUtil.newArrayList("a", "b", "b", "b", "c", "d"); + + Collection union = CollectionUtil.union(list1, list2); + + Assert.assertEquals(3, CollectionUtil.count(union, new Matcher() { + + @Override + public boolean match(String t) { + return t.equals("b"); + } + + })); + } + + @Test + public void intersectionTest() { + ArrayList list1 = CollectionUtil.newArrayList("a", "b", "b", "c", "d", "x"); + ArrayList list2 = CollectionUtil.newArrayList("a", "b", "b", "b", "c", "d"); + + Collection union = CollectionUtil.intersection(list1, list2); + Assert.assertEquals(2, CollectionUtil.count(union, new Matcher() { + + @Override + public boolean match(String t) { + return t.equals("b"); + } + + })); + } + + @Test + public void disjunctionTest() { + ArrayList list1 = CollectionUtil.newArrayList("a", "b", "b", "c", "d", "x"); + ArrayList list2 = CollectionUtil.newArrayList("a", "b", "b", "b", "c", "d", "x2"); + + Collection disjunction = CollectionUtil.disjunction(list1, list2); + Assert.assertTrue(disjunction.contains("b")); + Assert.assertTrue(disjunction.contains("x2")); + Assert.assertTrue(disjunction.contains("x")); + + Collection disjunction2 = CollectionUtil.disjunction(list2, list1); + Assert.assertTrue(disjunction2.contains("b")); + Assert.assertTrue(disjunction2.contains("x2")); + Assert.assertTrue(disjunction2.contains("x")); + } + + @Test + public void disjunctionTest2() { + // 任意一个集合为空,差集为另一个集合 + ArrayList list1 = CollectionUtil.newArrayList(); + ArrayList list2 = CollectionUtil.newArrayList("a", "b", "b", "b", "c", "d", "x2"); + + Collection disjunction = CollectionUtil.disjunction(list1, list2); + Assert.assertEquals(list2, disjunction); + Collection disjunction2 = CollectionUtil.disjunction(list2, list1); + Assert.assertEquals(list2, disjunction2); + } + + @Test + public void disjunctionTest3() { + // 无交集下返回共同的元素 + ArrayList list1 = CollectionUtil.newArrayList("1", "2", "3"); + ArrayList list2 = CollectionUtil.newArrayList("a", "b", "c"); + + Collection disjunction = CollectionUtil.disjunction(list1, list2); + Assert.assertTrue(disjunction.contains("1")); + Assert.assertTrue(disjunction.contains("2")); + Assert.assertTrue(disjunction.contains("3")); + Assert.assertTrue(disjunction.contains("a")); + Assert.assertTrue(disjunction.contains("b")); + Assert.assertTrue(disjunction.contains("c")); + Collection disjunction2 = CollectionUtil.disjunction(list2, list1); + Assert.assertTrue(disjunction2.contains("1")); + Assert.assertTrue(disjunction2.contains("2")); + Assert.assertTrue(disjunction2.contains("3")); + Assert.assertTrue(disjunction2.contains("a")); + Assert.assertTrue(disjunction2.contains("b")); + Assert.assertTrue(disjunction2.contains("c")); + } + + @Test + public void toMapListAndToListMapTest() { + HashMap map1 = new HashMap<>(); + map1.put("a", "值1"); + map1.put("b", "值1"); + + HashMap map2 = new HashMap<>(); + map2.put("a", "值2"); + map2.put("c", "值3"); + + // ---------------------------------------------------------------------------------------- + ArrayList> list = CollectionUtil.newArrayList(map1, map2); + Map> map = CollectionUtil.toListMap(list); + Assert.assertEquals("值1", map.get("a").get(0)); + Assert.assertEquals("值2", map.get("a").get(1)); + + // ---------------------------------------------------------------------------------------- + List> listMap = CollectionUtil.toMapList(map); + Assert.assertEquals("值1", listMap.get(0).get("a")); + Assert.assertEquals("值2", listMap.get(1).get("a")); + } + + @Test + public void getFieldValuesTest() { + Dict v1 = Dict.create().set("id", 12).set("name", "张三").set("age", 23); + Dict v2 = Dict.create().set("age", 13).set("id", 15).set("name", "李四"); + ArrayList list = CollectionUtil.newArrayList(v1, v2); + + List fieldValues = CollectionUtil.getFieldValues(list, "name"); + Assert.assertEquals("张三", fieldValues.get(0)); + Assert.assertEquals("李四", fieldValues.get(1)); + } + + @Test + public void splitTest() { + final ArrayList list = CollectionUtil.newArrayList(1, 2, 3, 4, 5, 6, 7, 8, 9); + List> split = CollectionUtil.split(list, 3); + Assert.assertEquals(3, split.size()); + Assert.assertEquals(3, split.get(0).size()); + } + + @Test + public void foreachTest() { + HashMap map = MapUtil.newHashMap(); + map.put("a", "1"); + map.put("b", "2"); + map.put("c", "3"); + + final String[] result = new String[1]; + CollectionUtil.forEach(map, new CollUtil.KVConsumer() { + @Override + public void accept(String key, String value, int index) { + if (key.equals("a")) { + result[0] = value; + } + } + }); + Assert.assertEquals("1", result[0]); + } + + @Test + public void filterTest() { + ArrayList list = CollUtil.newArrayList("a", "b", "c"); + + Collection filtered = CollUtil.filter(list, new Editor() { + @Override + public String edit(String t) { + return t + 1; + } + }); + + Assert.assertEquals(CollUtil.newArrayList("a1", "b1", "c1"), filtered); + } + + @Test + public void groupTest() { + List list = CollUtil.newArrayList("1", "2", "3", "4", "5", "6"); + List> group = CollectionUtil.group(list, null); + Assert.assertTrue(group.size() > 0); + + List> group2 = CollectionUtil.group(list, new Hash() { + @Override + public int hash(String t) { + // 按照奇数偶数分类 + return Integer.parseInt(t) % 2; + } + + }); + Assert.assertEquals(CollUtil.newArrayList("2", "4", "6"), group2.get(0)); + Assert.assertEquals(CollUtil.newArrayList("1", "3", "5"), group2.get(1)); + } + + @Test + public void groupByFieldTest() { + List list = CollUtil.newArrayList(new TestBean("张三", 12), new TestBean("李四", 13), new TestBean("王五", 12)); + List> groupByField = CollUtil.groupByField(list, "age"); + Assert.assertEquals("张三", groupByField.get(0).get(0).getName()); + Assert.assertEquals("王五", groupByField.get(0).get(1).getName()); + + Assert.assertEquals("李四", groupByField.get(1).get(0).getName()); + } + + @Test + public void sortByPropertyTest() { + List list = CollUtil.newArrayList(new TestBean("张三", 12, DateUtil.parse("2018-05-01")), // + new TestBean("李四", 13, DateUtil.parse("2018-03-01")), // + new TestBean("王五", 12, DateUtil.parse("2018-04-01"))// + ); + + CollUtil.sortByProperty(list, "createTime"); + Assert.assertEquals("李四", list.get(0).getName()); + Assert.assertEquals("王五", list.get(1).getName()); + Assert.assertEquals("张三", list.get(2).getName()); + } + + public static class TestBean { + private String name; + private int age; + private Date createTime; + + public TestBean(String name, int age) { + this.name = name; + this.age = age; + } + + public TestBean(String name, int age, Date createTime) { + this.name = name; + this.age = age; + this.createTime = createTime; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public Date getCreateTime() { + return createTime; + } + + public void setCreateTime(Date createTime) { + this.createTime = createTime; + } + + @Override + public String toString() { + return "TestBeans [name=" + name + ", age=" + age + "]"; + } + } + + @Test + public void listTest() { + List list1 = CollUtil.list(false); + List list2 = CollUtil.list(true); + + Assert.assertTrue(list1 instanceof ArrayList); + Assert.assertTrue(list2 instanceof LinkedList); + } + + @Test + public void listTest2() { + List list1 = CollUtil.list(false, "a", "b", "c"); + List list2 = CollUtil.list(true, "a", "b", "c"); + Assert.assertEquals("[a, b, c]", list1.toString()); + Assert.assertEquals("[a, b, c]", list2.toString()); + } + + @Test + public void listTest3() { + HashSet set = new LinkedHashSet<>(); + set.add("a"); + set.add("b"); + set.add("c"); + + List list1 = CollUtil.list(false, set); + List list2 = CollUtil.list(true, set); + Assert.assertEquals("[a, b, c]", list1.toString()); + Assert.assertEquals("[a, b, c]", list2.toString()); + } + + @Test + public void getTest() { + HashSet set = CollUtil.newHashSet(true, new String[] { "A", "B", "C", "D" }); + String str = CollUtil.get(set, 2); + Assert.assertEquals("C", str); + + str = CollUtil.get(set, -1); + Assert.assertEquals("D", str); + } + + @Test + public void addAllIfNotContainsTest() { + ArrayList list1 = new ArrayList<>(); + list1.add("1"); + list1.add("2"); + ArrayList list2 = new ArrayList<>(); + list2.add("2"); + list2.add("3"); + CollUtil.addAllIfNotContains(list1, list2); + + Assert.assertEquals(3, list1.size()); + Assert.assertEquals("1", list1.get(0)); + Assert.assertEquals("2", list1.get(1)); + Assert.assertEquals("3", list1.get(2)); + } + + @Test + public void subInput1PositiveNegativePositiveOutput1() { + // Arrange + final List list = new ArrayList<>(); + list.add(null); + final int start = 3; + final int end = -1; + final int step = 2; + // Act + final List retval = CollUtil.sub(list, start, end, step); + // Assert result + final List arrayList = new ArrayList<>(); + arrayList.add(null); + Assert.assertEquals(arrayList, retval); + } + + @Test + public void subInput1ZeroPositivePositiveOutput1() { + // Arrange + final List list = new ArrayList<>(); + list.add(null); + final int start = 0; + final int end = 1; + final int step = 2; + // Act + final List retval = CollUtil.sub(list, start, end, step); + + // Assert result + final List arrayList = new ArrayList<>(); + arrayList.add(null); + Assert.assertEquals(arrayList, retval); + } + + @Test + public void subInput1PositiveZeroOutput0() { + // Arrange + final List list = new ArrayList<>(); + list.add(null); + final int start = 1; + final int end = 0; + // Act + final List retval = CollUtil.sub(list, start, end); + + // Assert result + final List arrayList = new ArrayList<>(); + Assert.assertEquals(arrayList, retval); + } + + @Test + public void subInput0ZeroZeroZeroOutputNull() { + // Arrange + final List list = new ArrayList<>(); + final int start = 0; + final int end = 0; + final int step = 0; + // Act + final List retval = CollUtil.sub(list, start, end, step); + // Assert result + Assert.assertTrue(retval.isEmpty()); + } + + @Test + public void subInput1PositiveNegativeZeroOutput0() { + // Arrange + final List list = new ArrayList<>(); + list.add(null); + final int start = 1; + final int end = -2_147_483_648; + final int step = 0; + // Act + final List retval = CollUtil.sub(list, start, end, step); + // Assert result + final List arrayList = new ArrayList<>(); + Assert.assertEquals(arrayList, retval); + } + + @Test + public void subInput1PositivePositivePositiveOutput0() { + // Arrange + final List list = new ArrayList<>(); + list.add(null); + final int start = 2_147_483_647; + final int end = 2_147_483_647; + final int step = 1_073_741_824; + // Act + final List retval = CollUtil.sub(list, start, end, step); + // Assert result + final List arrayList = new ArrayList<>(); + Assert.assertEquals(arrayList, retval); + } + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void subInput1PositiveNegativePositiveOutputArrayIndexOutOfBoundsException() { + // Arrange + final List list = new ArrayList<>(); + list.add(null); + final int start = 2_147_483_643; + final int end = -2_147_483_648; + final int step = 2; + + // Act + thrown.expect(ArrayIndexOutOfBoundsException.class); + CollUtil.sub(list, start, end, step); + // Method is not expected to return due to exception thrown + } + + @Test + public void subInput0ZeroPositiveNegativeOutputNull() { + // Arrange + final List list = new ArrayList<>(); + final int start = 0; + final int end = 1; + final int step = -2_147_483_646; + // Act + final List retval = CollUtil.sub(list, start, end, step); + // Assert result + Assert.assertTrue(retval.isEmpty()); + } + + @Test + public void subInput1PositivePositivePositiveOutput02() { + // Arrange + final List list = new ArrayList<>(); + list.add(null); + final int start = 2_147_483_643; + final int end = 2_147_483_642; + final int step = 1_073_741_824; + // Act + final List retval = CollUtil.sub(list, start, end, step); + // Assert result + final List arrayList = new ArrayList<>(); + Assert.assertEquals(arrayList, retval); + } + + @Test + public void subInput1ZeroZeroPositiveOutput0() { + // Arrange + final List list = new ArrayList<>(); + list.add(0); + final int start = 0; + final int end = 0; + final int step = 2; + // Act + final List retval = CollUtil.sub(list, start, end, step); + // Assert result + final List arrayList = new ArrayList<>(); + Assert.assertEquals(arrayList, retval); + } + + @Test + public void subInput1NegativeZeroPositiveOutput0() { + // Arrange + final List list = new ArrayList<>(); + list.add(0); + final int start = -1; + final int end = 0; + final int step = 2; + // Act + final List retval = CollUtil.sub(list, start, end, step); + // Assert result + final List arrayList = new ArrayList<>(); + Assert.assertEquals(arrayList, retval); + } + + @Test + public void subInput0ZeroZeroOutputNull() { + // Arrange + final List list = new ArrayList<>(); + final int start = 0; + final int end = 0; + // Act + final List retval = CollUtil.sub(list, start, end); + // Assert result + Assert.assertTrue(retval.isEmpty()); + } + + @Test + public void sortPageAllTest() { + ArrayList list = CollUtil.newArrayList(1, 2, 3, 4, 5, 6, 7, 8, 9); + List sortPageAll = CollUtil.sortPageAll(2, 5, new Comparator() { + @Override + public int compare(Integer o1, Integer o2) { + // 反序 + return o2.compareTo(o1); + } + }, list); + + Assert.assertEquals(CollUtil.newArrayList(4, 3, 2, 1), sortPageAll); + } + + @Test + public void containsAnyTest() { + ArrayList list1 = CollUtil.newArrayList(1, 2, 3, 4, 5); + ArrayList list2 = CollUtil.newArrayList(5, 3, 1, 9, 11); + + Assert.assertTrue(CollUtil.containsAny(list1, list2)); + } + + @Test + public void containsAllTest() { + ArrayList list1 = CollUtil.newArrayList(1, 2, 3, 4, 5); + ArrayList list2 = CollUtil.newArrayList(5, 3, 1); + + Assert.assertTrue(CollUtil.containsAll(list1, list2)); + } + + @Test + public void getLastTest() { + // 测试:空数组返回null而不是报错 + List test = CollUtil.newArrayList(); + String last = CollUtil.getLast(test); + Assert.assertNull(last); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/collection/IterUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/collection/IterUtilTest.java new file mode 100644 index 000000000..11351f461 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/collection/IterUtilTest.java @@ -0,0 +1,77 @@ +package cn.hutool.core.collection; + +import java.util.ArrayList; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +/** + * {@link IterUtil} 单元测试 + * @author looly + * + */ +public class IterUtilTest { + + @Test + public void countMapTest() { + ArrayList list = CollUtil.newArrayList("a", "b", "c", "c", "a", "b", "d"); + Map countMap = IterUtil.countMap(list); + + Assert.assertEquals(Integer.valueOf(2), countMap.get("a")); + Assert.assertEquals(Integer.valueOf(2), countMap.get("b")); + Assert.assertEquals(Integer.valueOf(2), countMap.get("c")); + Assert.assertEquals(Integer.valueOf(1), countMap.get("d")); + } + + @Test + public void fieldValueMapTest() { + ArrayList carList = CollUtil.newArrayList(new Car("123", "大众"), new Car("345", "奔驰"), new Car("567", "路虎")); + Map carNameMap = IterUtil.fieldValueMap(carList, "carNumber"); + + Assert.assertEquals("大众", carNameMap.get("123").getCarName()); + Assert.assertEquals("奔驰", carNameMap.get("345").getCarName()); + Assert.assertEquals("路虎", carNameMap.get("567").getCarName()); + } + + @Test + public void joinTest() { + ArrayList list = CollUtil.newArrayList("1", "2", "3", "4"); + String join = IterUtil.join(list, ":"); + Assert.assertEquals("1:2:3:4", join); + + ArrayList list1 = CollUtil.newArrayList(1, 2, 3, 4); + String join1 = IterUtil.join(list1, ":"); + Assert.assertEquals("1:2:3:4", join1); + + ArrayList list2 = CollUtil.newArrayList("1", "2", "3", "4"); + String join2 = IterUtil.join(list2, ":", "\"", "\""); + Assert.assertEquals("\"1\":\"2\":\"3\":\"4\"", join2); + } + + public static class Car { + private String carNumber; + private String carName; + + public Car(String carNumber, String carName) { + this.carNumber = carNumber; + this.carName = carName; + } + + public String getCarNumber() { + return carNumber; + } + + public void setCarNumber(String carNumber) { + this.carNumber = carNumber; + } + + public String getCarName() { + return carName; + } + + public void setCarName(String carName) { + this.carName = carName; + } + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/collection/MapProxyTest.java b/hutool-core/src/test/java/cn/hutool/core/collection/MapProxyTest.java new file mode 100644 index 000000000..c8f5e1b9e --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/collection/MapProxyTest.java @@ -0,0 +1,31 @@ +package cn.hutool.core.collection; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.map.MapProxy; + +public class MapProxyTest { + + @Test + public void mapProxyTest() { + Map map = new HashMap<>(); + map.put("a", "1"); + map.put("b", "2"); + + MapProxy mapProxy = new MapProxy(map); + Integer b = mapProxy.getInt("b"); + Assert.assertEquals(new Integer(2), b); + + Set keys = mapProxy.keySet(); + Assert.assertFalse(keys.isEmpty()); + + Set> entrys = mapProxy.entrySet(); + Assert.assertFalse(entrys.isEmpty()); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/comparator/VersionComparatorTest.java b/hutool-core/src/test/java/cn/hutool/core/comparator/VersionComparatorTest.java new file mode 100644 index 000000000..cbfd95da0 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/comparator/VersionComparatorTest.java @@ -0,0 +1,49 @@ +package cn.hutool.core.comparator; + +import org.junit.Assert; +import org.junit.Test; + +/** + * 版本比较单元测试 + * + * @author looly + * + */ +public class VersionComparatorTest { + + @Test + public void versionComparatorTest1() { + int compare = VersionComparator.INSTANCE.compare("1.2.1", "1.12.1"); + Assert.assertTrue(compare < 0); + } + + @Test + public void versionComparatorTest2() { + int compare = VersionComparator.INSTANCE.compare("1.12.1", "1.12.1c"); + Assert.assertTrue(compare < 0); + } + + @Test + public void versionComparatorTest3() { + int compare = VersionComparator.INSTANCE.compare(null, "1.12.1c"); + Assert.assertTrue(compare < 0); + } + + @Test + public void versionComparatorTest4() { + int compare = VersionComparator.INSTANCE.compare("1.13.0", "1.12.1c"); + Assert.assertTrue(compare > 0); + } + + @Test + public void versionComparatorTest5() { + int compare = VersionComparator.INSTANCE.compare("V1.2", "V1.1"); + Assert.assertTrue(compare > 0); + } + + @Test + public void versionComparatorTes6() { + int compare = VersionComparator.INSTANCE.compare("V0.0.20170102", "V0.0.20170101"); + Assert.assertTrue(compare > 0); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/convert/ConvertOtherTest.java b/hutool-core/src/test/java/cn/hutool/core/convert/ConvertOtherTest.java new file mode 100644 index 000000000..c13809343 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/convert/ConvertOtherTest.java @@ -0,0 +1,89 @@ +package cn.hutool.core.convert; + +import java.util.concurrent.TimeUnit; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.util.CharsetUtil; + +/** + * 其它转换 + * @author Looly + * + */ +public class ConvertOtherTest { + @Test + public void hexTest() { + String a = "我是一个小小的可爱的字符串"; + String hex = Convert.toHex(a, CharsetUtil.CHARSET_UTF_8); + Assert.assertEquals("e68891e698afe4b880e4b8aae5b08fe5b08fe79a84e58fafe788b1e79a84e5ad97e7aca6e4b8b2", hex); + + String raw = Convert.hexToStr(hex, CharsetUtil.CHARSET_UTF_8); + Assert.assertEquals(a, raw); + } + + @Test + public void unicodeTest() { + String a = "我是一个小小的可爱的字符串"; + + String unicode = Convert.strToUnicode(a); + Assert.assertEquals("\\u6211\\u662f\\u4e00\\u4e2a\\u5c0f\\u5c0f\\u7684\\u53ef\\u7231\\u7684\\u5b57\\u7b26\\u4e32", unicode); + + String raw = Convert.unicodeToStr(unicode); + Assert.assertEquals(raw, a); + + // 针对有特殊空白符的Unicode + String str = "你 好"; + String unicode2 = Convert.strToUnicode(str); + Assert.assertEquals("\\u4f60\\u00a0\\u597d", unicode2); + + String str2 = Convert.unicodeToStr(unicode2); + Assert.assertEquals(str, str2); + } + + @Test + public void convertCharsetTest() { + String a = "我不是乱码"; + // 转换后result为乱码 + String result = Convert.convertCharset(a, CharsetUtil.UTF_8, CharsetUtil.ISO_8859_1); + String raw = Convert.convertCharset(result, CharsetUtil.ISO_8859_1, "UTF-8"); + Assert.assertEquals(raw, a); + } + + @Test + public void convertTimeTest() { + long a = 4535345; + long minutes = Convert.convertTime(a, TimeUnit.MILLISECONDS, TimeUnit.MINUTES); + Assert.assertEquals(75, minutes); + } + + @Test + public void digitToChineseTest() { + double a = 67556.32; + String digitUppercase = Convert.digitToChinese(a); + Assert.assertEquals("陆万柒仟伍佰伍拾陆元叁角贰分", digitUppercase); + + a = 1024.00; + digitUppercase = Convert.digitToChinese(a); + Assert.assertEquals("壹仟零贰拾肆元整", digitUppercase); + + a = 1024; + digitUppercase = Convert.digitToChinese(a); + Assert.assertEquals("壹仟零贰拾肆元整", digitUppercase); + } + + @Test + public void wrapUnwrapTest() { + // 去包装 + Class wrapClass = Integer.class; + Class unWraped = Convert.unWrap(wrapClass); + Assert.assertEquals(int.class, unWraped); + + // 包装 + Class primitiveClass = long.class; + Class wraped = Convert.wrap(primitiveClass); + Assert.assertEquals(Long.class, wraped); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/convert/ConvertTest.java b/hutool-core/src/test/java/cn/hutool/core/convert/ConvertTest.java new file mode 100644 index 000000000..617585eed --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/convert/ConvertTest.java @@ -0,0 +1,205 @@ +package cn.hutool.core.convert; + +import java.util.ArrayList; +import java.util.Date; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.date.DateUtil; + +/** + * 类型转换工具单元测试 + * + * @author Looly + * + */ +public class ConvertTest { + + @Test + public void toObjectTest() { + Object result = Convert.convert(Object.class, "aaaa"); + Assert.assertEquals("aaaa", result); + } + + @Test + public void toStrTest() { + int a = 1; + long[] b = { 1, 2, 3, 4, 5 }; + + String aStr = Convert.toStr(a); + Assert.assertEquals("1", aStr); + String bStr = Convert.toStr(b); + Assert.assertEquals("[1, 2, 3, 4, 5]", Convert.toStr(bStr)); + } + + @Test + public void toStrTest2() { + String result = Convert.convert(String.class, "aaaa"); + Assert.assertEquals("aaaa", result); + } + + @Test + public void toStrTest3() { + char a = 'a'; + String result = Convert.convert(String.class, a); + Assert.assertEquals("a", result); + } + + @Test + public void toIntTest() { + String a = " 34232"; + Integer aInteger = Convert.toInt(a); + Assert.assertEquals(Integer.valueOf(34232), aInteger); + int aInt = ConverterRegistry.getInstance().convert(int.class, a); + Assert.assertTrue(34232 == aInt); + + // 带小数测试 + String b = " 34232.00"; + Integer bInteger = Convert.toInt(b); + Assert.assertEquals(Integer.valueOf(34232), bInteger); + int bInt = ConverterRegistry.getInstance().convert(int.class, b); + Assert.assertTrue(34232 == bInt); + + // boolean测试 + boolean c = true; + Integer cInteger = Convert.toInt(c); + Assert.assertEquals(Integer.valueOf(1), cInteger); + int cInt = ConverterRegistry.getInstance().convert(int.class, c); + Assert.assertTrue(1 == cInt); + + // boolean测试 + String d = "08"; + Integer dInteger = Convert.toInt(d); + Assert.assertEquals(Integer.valueOf(8), dInteger); + int dInt = ConverterRegistry.getInstance().convert(int.class, d); + Assert.assertTrue(8 == dInt); + } + + @Test + public void toIntTest2() { + ArrayList array = new ArrayList<>(); + Integer aInt = Convert.convertQuietly(Integer.class, array, -1); + Assert.assertEquals(Integer.valueOf(-1), aInt); + } + + @Test + public void toLongTest() { + String a = " 342324545435435"; + Long aLong = Convert.toLong(a); + Assert.assertEquals(Long.valueOf(342324545435435L), aLong); + long aLong2 = ConverterRegistry.getInstance().convert(long.class, a); + Assert.assertTrue(342324545435435L == aLong2); + + // 带小数测试 + String b = " 342324545435435.245435435"; + Long bLong = Convert.toLong(b); + Assert.assertEquals(Long.valueOf(342324545435435L), bLong); + long bLong2 = ConverterRegistry.getInstance().convert(long.class, b); + Assert.assertTrue(342324545435435L == bLong2); + + // boolean测试 + boolean c = true; + Long cLong = Convert.toLong(c); + Assert.assertEquals(Long.valueOf(1), cLong); + long cLong2 = ConverterRegistry.getInstance().convert(long.class, c); + Assert.assertTrue(1 == cLong2); + + // boolean测试 + String d = "08"; + Long dLong = Convert.toLong(d); + Assert.assertEquals(Long.valueOf(8), dLong); + long dLong2 = ConverterRegistry.getInstance().convert(long.class, d); + Assert.assertTrue(8 == dLong2); + } + + @Test + public void toCharTest() { + String str = "aadfdsfs"; + Character c = Convert.toChar(str); + Assert.assertEquals(Character.valueOf('a'), c); + + // 转换失败 + Object str2 = ""; + Character c2 = Convert.toChar(str2); + Assert.assertNull(c2); + } + + @Test + public void toNumberTest() { + Object a = "12.45"; + Number number = Convert.toNumber(a); + Assert.assertEquals(12.45D, number); + } + + @Test + public void emptyToNumberTest() { + Object a = ""; + Number number = Convert.toNumber(a); + Assert.assertNull(number); + } + + @Test + public void toDateTest() { + String a = "2017-05-06"; + Date value = Convert.toDate(a); + Assert.assertEquals(a, DateUtil.formatDate(value)); + + long timeLong = DateUtil.date().getTime(); + Date value2 = Convert.toDate(timeLong); + Assert.assertEquals(timeLong, value2.getTime()); + } + + @Test + public void toSqlDateTest() { + String a = "2017-05-06"; + java.sql.Date value = Convert.convert(java.sql.Date.class, a); + Assert.assertEquals("2017-05-06", value.toString()); + + long timeLong = DateUtil.date().getTime(); + java.sql.Date value2 = Convert.convert(java.sql.Date.class, timeLong); + Assert.assertEquals(timeLong, value2.getTime()); + } + + @Test + public void intAndByteConvertTest() { + // 测试 int 转 byte + int int0 = 234; + byte byte0 = Convert.intToByte(int0); + Assert.assertEquals(-22, byte0); + + int int1 = Convert.byteToUnsignedInt(byte0); + Assert.assertEquals(int0, int1); + } + + @Test + public void intAndBytesTest() { + // 测试 int 转 byte 数组 + int int2 = 1417; + byte[] bytesInt = Convert.intToBytes(int2); + + // 测试 byte 数组转 int + int int3 = Convert.bytesToInt(bytesInt); + Assert.assertEquals(int2, int3); + } + + @Test + public void longAndBytesTest() { + // 测试 long 转 byte 数组 + long long1 = 2223; + + byte[] bytesLong = Convert.longToBytes(long1); + long long2 = Convert.bytesToLong(bytesLong); + + Assert.assertEquals(long1, long2); + } + + @Test + public void shortAndBytesTest() { + short short1 = 122; + byte[] bytes = Convert.shortToBytes(short1); + short short2 = Convert.bytesToShort(bytes); + + Assert.assertEquals(short2, short1); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/convert/ConvertToArrayTest.java b/hutool-core/src/test/java/cn/hutool/core/convert/ConvertToArrayTest.java new file mode 100644 index 000000000..8670e4c83 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/convert/ConvertToArrayTest.java @@ -0,0 +1,135 @@ +package cn.hutool.core.convert; + +import java.io.File; +import java.net.URL; +import java.util.ArrayList; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.convert.impl.ArrayConverter; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.lang.Console; + +/** + * 类型转换工具单元测试
+ * 转换为数组 + * + * @author Looly + * + */ +public class ConvertToArrayTest { + + @Test + public void toIntArrayTest() { + String[] b = { "1", "2", "3", "4" }; + + Integer[] integerArray = Convert.toIntArray(b); + Assert.assertArrayEquals(integerArray, new Integer[]{1,2,3,4}); + + int[] intArray = Convert.convert(int[].class, b); + Assert.assertArrayEquals(intArray, new int[]{1,2,3,4}); + + long[] c = {1,2,3,4,5}; + Integer[] intArray2 = Convert.toIntArray(c); + Assert.assertArrayEquals(intArray2, new Integer[]{1,2,3,4,5}); + } + + @Test + public void toLongArrayTest() { + String[] b = { "1", "2", "3", "4" }; + + Long[] longArray = Convert.toLongArray(b); + Assert.assertArrayEquals(longArray, new Long[]{1L,2L,3L,4L}); + + long[] longArray2 = Convert.convert(long[].class, b); + Assert.assertArrayEquals(longArray2, new long[]{1L,2L,3L,4L}); + + int[] c = {1,2,3,4,5}; + Long[] intArray2 = Convert.toLongArray(c); + Assert.assertArrayEquals(intArray2, new Long[]{1L,2L,3L,4L,5L}); + } + + @Test + public void toDoubleArrayTest() { + String[] b = { "1", "2", "3", "4" }; + + Double[] doubleArray = Convert.toDoubleArray(b); + Assert.assertArrayEquals(doubleArray, new Double[]{1D,2D,3D,4D}); + + double[] doubleArray2 = Convert.convert(double[].class, b); + Assert.assertArrayEquals(doubleArray2, new double[]{1D,2D,3D,4D}, 2); + + int[] c = {1,2,3,4,5}; + Double[] intArray2 = Convert.toDoubleArray(c); + Assert.assertArrayEquals(intArray2, new Double[]{1D,2D,3D,4D,5D}); + } + + @Test + public void toPrimitiveArrayTest(){ + + //数组转数组测试 + int[] a = new int[]{1,2,3,4}; + long[] result = ConverterRegistry.getInstance().convert(long[].class, a); + Assert.assertArrayEquals(new long[]{1L, 2L, 3L, 4L}, result); + + //数组转数组测试 + byte[] resultBytes = ConverterRegistry.getInstance().convert(byte[].class, a); + Assert.assertArrayEquals(new byte[]{1, 2, 3, 4}, resultBytes); + + //字符串转数组 + String arrayStr = "1,2,3,4,5"; + //获取Converter类的方法2,自己实例化相应Converter对象 + ArrayConverter c3 = new ArrayConverter(int[].class); + int[] result3 = (int[]) c3.convert(arrayStr, null); + Assert.assertArrayEquals(new int[]{1,2,3,4,5}, result3); + } + + @Test + public void collectionToArrayTest() { + ArrayList list = new ArrayList<>(); + list.add("a"); + list.add("b"); + list.add("c"); + + String[] result = Convert.toStrArray(list); + Assert.assertEquals(list.get(0), result[0]); + Assert.assertEquals(list.get(1), result[1]); + Assert.assertEquals(list.get(2), result[2]); + } + + @Test + public void strToCharArrayTest() { + String testStr = "abcde"; + Character[] array = Convert.toCharArray(testStr); + + //包装类型数组 + Assert.assertEquals(new Character('a'), array[0]); + Assert.assertEquals(new Character('b'), array[1]); + Assert.assertEquals(new Character('c'), array[2]); + Assert.assertEquals(new Character('d'), array[3]); + Assert.assertEquals(new Character('e'), array[4]); + + //原始类型数组 + char[] array2 = Convert.convert(char[].class, testStr); + Assert.assertEquals('a', array2[0]); + Assert.assertEquals('b', array2[1]); + Assert.assertEquals('c', array2[2]); + Assert.assertEquals('d', array2[3]); + Assert.assertEquals('e', array2[4]); + + } + + @Test + @Ignore + public void toUrlArrayTest() { + File[] files = FileUtil.file("D:\\workspace").listFiles(); + + URL[] urls = Convert.convert(URL[].class, files); + + for (URL url : urls) { + Console.log(url.getPath()); + } + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/convert/ConvertToBeanTest.java b/hutool-core/src/test/java/cn/hutool/core/convert/ConvertToBeanTest.java new file mode 100644 index 000000000..201b813a4 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/convert/ConvertToBeanTest.java @@ -0,0 +1,50 @@ +package cn.hutool.core.convert; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.bean.BeanUtilTest.SubPerson; + +/** + * 类型转换工具单元测试
+ * 转换为数组 + * + * @author Looly + * + */ +public class ConvertToBeanTest { + + @Test + public void beanToMapTest() { + SubPerson person = new SubPerson(); + person.setAge(14); + person.setOpenid("11213232"); + person.setName("测试A11"); + person.setSubName("sub名字"); + + Map map = Convert.convert(Map.class, person); + Assert.assertEquals(map.get("name"), "测试A11"); + Assert.assertEquals(map.get("age"), 14); + Assert.assertEquals("11213232", map.get("openid")); + } + + @Test + public void mapToBeanTest() { + HashMap map = new HashMap<>(); + map.put("id", "88dc4b28-91b1-4a1a-bab5-444b795c7ecd"); + map.put("age", 14); + map.put("openid", "11213232"); + map.put("name", "测试A11"); + map.put("subName", "sub名字"); + + SubPerson subPerson = Convert.convert(SubPerson.class, map); + Assert.assertEquals("88dc4b28-91b1-4a1a-bab5-444b795c7ecd", subPerson.getId().toString()); + Assert.assertEquals(14, subPerson.getAge()); + Assert.assertEquals("11213232", subPerson.getOpenid()); + Assert.assertEquals("测试A11", subPerson.getName()); + Assert.assertEquals("11213232", subPerson.getOpenid()); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/convert/ConvertToCollectionTest.java b/hutool-core/src/test/java/cn/hutool/core/convert/ConvertToCollectionTest.java new file mode 100644 index 000000000..778a3fbff --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/convert/ConvertToCollectionTest.java @@ -0,0 +1,139 @@ +package cn.hutool.core.convert; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.TypeReference; + +/** + * 转换为集合测试 + * + * @author looly + * + */ +public class ConvertToCollectionTest { + + @Test + public void toCollectionTest() { + Object[] a = { "a", "你", "好", "", 1 }; + List list = (List) Convert.convert(Collection.class, a); + Assert.assertEquals("a", list.get(0)); + Assert.assertEquals("你", list.get(1)); + Assert.assertEquals("好", list.get(2)); + Assert.assertEquals("", list.get(3)); + Assert.assertEquals(1, list.get(4)); + } + + @Test + public void toListTest() { + Object[] a = { "a", "你", "好", "", 1 }; + List list = Convert.toList(a); + Assert.assertEquals("a", list.get(0)); + Assert.assertEquals("你", list.get(1)); + Assert.assertEquals("好", list.get(2)); + Assert.assertEquals("", list.get(3)); + Assert.assertEquals(1, list.get(4)); + } + + @Test + public void toListTest2() { + Object[] a = { "a", "你", "好", "", 1 }; + List list = Convert.toList(String.class, a); + Assert.assertEquals("a", list.get(0)); + Assert.assertEquals("你", list.get(1)); + Assert.assertEquals("好", list.get(2)); + Assert.assertEquals("", list.get(3)); + Assert.assertEquals("1", list.get(4)); + } + + @Test + public void toListTest3() { + Object[] a = { "a", "你", "好", "", 1 }; + List list = Convert.toList(String.class, a); + Assert.assertEquals("a", list.get(0)); + Assert.assertEquals("你", list.get(1)); + Assert.assertEquals("好", list.get(2)); + Assert.assertEquals("", list.get(3)); + Assert.assertEquals("1", list.get(4)); + } + + @Test + public void toListTest4() { + Object[] a = { "a", "你", "好", "", 1 }; + List list = Convert.convert(new TypeReference>() {}, a); + Assert.assertEquals("a", list.get(0)); + Assert.assertEquals("你", list.get(1)); + Assert.assertEquals("好", list.get(2)); + Assert.assertEquals("", list.get(3)); + Assert.assertEquals("1", list.get(4)); + } + + @Test + public void strToListTest() { + String a = "a,你,好,123"; + List list = Convert.toList(a); + Assert.assertEquals(4, list.size()); + Assert.assertEquals("a", list.get(0)); + Assert.assertEquals("你", list.get(1)); + Assert.assertEquals("好", list.get(2)); + Assert.assertEquals("123", list.get(3)); + + String b = "a"; + List list2 = Convert.toList(b); + Assert.assertEquals(1, list2.size()); + Assert.assertEquals("a", list2.get(0)); + } + + @Test + public void strToListTest2() { + String a = "a,你,好,123"; + List list = Convert.toList(String.class, a); + Assert.assertEquals(4, list.size()); + Assert.assertEquals("a", list.get(0)); + Assert.assertEquals("你", list.get(1)); + Assert.assertEquals("好", list.get(2)); + Assert.assertEquals("123", list.get(3)); + } + + @Test + public void numberToListTest() { + Integer i = 1; + ArrayList list = Convert.convert(ArrayList.class, i); + Assert.assertTrue(i == list.get(0)); + + BigDecimal b = BigDecimal.ONE; + ArrayList list2 = Convert.convert(ArrayList.class, b); + Assert.assertEquals(b, list2.get(0)); + } + + @Test + public void toLinkedListTest() { + Object[] a = { "a", "你", "好", "", 1 }; + List list = Convert.convert(LinkedList.class, a); + Assert.assertEquals("a", list.get(0)); + Assert.assertEquals("你", list.get(1)); + Assert.assertEquals("好", list.get(2)); + Assert.assertEquals("", list.get(3)); + Assert.assertEquals(1, list.get(4)); + } + + @Test + public void toSetTest() { + Object[] a = { "a", "你", "好", "", 1 }; + LinkedHashSet set = Convert.convert(LinkedHashSet.class, a); + ArrayList list = CollUtil.newArrayList(set); + Assert.assertEquals("a", list.get(0)); + Assert.assertEquals("你", list.get(1)); + Assert.assertEquals("好", list.get(2)); + Assert.assertEquals("", list.get(3)); + Assert.assertEquals(1, list.get(4)); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/convert/ConvertToSBCAndDBCTest.java b/hutool-core/src/test/java/cn/hutool/core/convert/ConvertToSBCAndDBCTest.java new file mode 100644 index 000000000..20cf14d54 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/convert/ConvertToSBCAndDBCTest.java @@ -0,0 +1,30 @@ +package cn.hutool.core.convert; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.convert.Convert; + +/** + * 类型转换工具单元测试 + * 全角半角转换 + * + * @author Looly + * + */ +public class ConvertToSBCAndDBCTest { + + @Test + public void toSBCTest() { + String a = "123456789"; + String sbc = Convert.toSBC(a); + Assert.assertEquals("123456789", sbc); + } + + @Test + public void toDBCTest() { + String a = "123456789"; + String dbc = Convert.toDBC(a); + Assert.assertEquals("123456789", dbc); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/convert/ConverterRegistryTest.java b/hutool-core/src/test/java/cn/hutool/core/convert/ConverterRegistryTest.java new file mode 100644 index 000000000..ba3b11b93 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/convert/ConverterRegistryTest.java @@ -0,0 +1,43 @@ +package cn.hutool.core.convert; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.convert.Converter; +import cn.hutool.core.convert.ConverterRegistry; + +/** + * ConverterRegistry 单元测试 + * @author Looly + * + */ +public class ConverterRegistryTest { + + @Test + public void getConverterTest() { + Converter converter = ConverterRegistry.getInstance().getConverter(CharSequence.class, false); + Assert.assertNotNull(converter); + } + + @Test + public void customTest(){ + int a = 454553; + ConverterRegistry converterRegistry = ConverterRegistry.getInstance(); + + CharSequence result = converterRegistry.convert(CharSequence.class, a); + Assert.assertEquals("454553", result); + + //此处做为示例自定义CharSequence转换,因为Hutool中已经提供CharSequence转换,请尽量不要替换 + //替换可能引发关联转换异常(例如覆盖CharSequence转换会影响全局) + converterRegistry.putCustom(CharSequence.class, CustomConverter.class); + result = converterRegistry.convert(CharSequence.class, a); + Assert.assertEquals("Custom: 454553", result); + } + + public static class CustomConverter implements Converter{ + @Override + public CharSequence convert(Object value, CharSequence defaultValue) throws IllegalArgumentException { + return "Custom: " + value.toString(); + } + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/convert/MapConvertTest.java b/hutool-core/src/test/java/cn/hutool/core/convert/MapConvertTest.java new file mode 100644 index 000000000..36b409627 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/convert/MapConvertTest.java @@ -0,0 +1,61 @@ +package cn.hutool.core.convert; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.map.MapBuilder; + +/** + * Map转换单元测试 + * + * @author looly + * + */ +public class MapConvertTest { + + @Test + public void beanToMapTest() { + User user = new User(); + user.setName("AAA"); + user.setAge(45); + + HashMap map = Convert.convert(HashMap.class, user); + Assert.assertEquals("AAA", map.get("name")); + Assert.assertEquals(45, map.get("age")); + } + + @Test + public void mapToMapTest() { + Map srcMap = MapBuilder.create(new HashMap()).put("name", "AAA").put("age", 45).map(); + + LinkedHashMap map = Convert.convert(LinkedHashMap.class, srcMap); + Assert.assertEquals("AAA", map.get("name")); + Assert.assertEquals(45, map.get("age")); + } + + public static class User { + private String name; + private int age; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/convert/NumberChineseFormaterTest.java b/hutool-core/src/test/java/cn/hutool/core/convert/NumberChineseFormaterTest.java new file mode 100644 index 000000000..70b101525 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/convert/NumberChineseFormaterTest.java @@ -0,0 +1,68 @@ +package cn.hutool.core.convert; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.convert.NumberChineseFormater; + +public class NumberChineseFormaterTest { + + @Test + public void formatTest() { + String f1 = NumberChineseFormater.format(10889.72356, false); + Assert.assertEquals("一万零八百八十九点七二", f1); + f1 = NumberChineseFormater.format(12653, false); + Assert.assertEquals("一万二千六百五十三", f1); + f1 = NumberChineseFormater.format(215.6387, false); + Assert.assertEquals("二百一十五点六四", f1); + f1 = NumberChineseFormater.format(1024, false); + Assert.assertEquals("一千零二十四", f1); + f1 = NumberChineseFormater.format(100350089, false); + Assert.assertEquals("一亿三十五万零八十九", f1); + f1 = NumberChineseFormater.format(1200, false); + Assert.assertEquals("一千二百", f1); + f1 = NumberChineseFormater.format(12, false); + Assert.assertEquals("一十二", f1); + f1 = NumberChineseFormater.format(0.05, false); + Assert.assertEquals("零点零五", f1); + } + + @Test + public void formatTest2() { + String f1 = NumberChineseFormater.format(-0.3, false, false); + Assert.assertEquals("负零点三", f1); + } + + @Test + public void formatTranditionalTest() { + String f1 = NumberChineseFormater.format(10889.72356, true); + Assert.assertEquals("壹万零捌佰捌拾玖点柒贰", f1); + f1 = NumberChineseFormater.format(12653, true); + Assert.assertEquals("壹万贰仟陆佰伍拾叁", f1); + f1 = NumberChineseFormater.format(215.6387, true); + Assert.assertEquals("贰佰壹拾伍点陆肆", f1); + f1 = NumberChineseFormater.format(1024, true); + Assert.assertEquals("壹仟零贰拾肆", f1); + f1 = NumberChineseFormater.format(100350089, true); + Assert.assertEquals("壹亿叁拾伍万零捌拾玖", f1); + f1 = NumberChineseFormater.format(1200, true); + Assert.assertEquals("壹仟贰佰", f1); + f1 = NumberChineseFormater.format(12, true); + Assert.assertEquals("壹拾贰", f1); + f1 = NumberChineseFormater.format(0.05, true); + Assert.assertEquals("零点零伍", f1); + } + + @Test + public void digitToChineseTest() { + String digitToChinese = Convert.digitToChinese(12412412412421.12); + Assert.assertEquals("壹拾贰万肆仟壹佰贰拾肆亿壹仟贰佰肆拾壹万贰仟肆佰贰拾壹元壹角贰分", digitToChinese); + + String digitToChinese2 = Convert.digitToChinese(12412412412421D); + Assert.assertEquals("壹拾贰万肆仟壹佰贰拾肆亿壹仟贰佰肆拾壹万贰仟肆佰贰拾壹元整", digitToChinese2); + + String digitToChinese3 = Convert.digitToChinese(2421.02); + Assert.assertEquals("贰仟肆佰贰拾壹元零贰分", digitToChinese3); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/convert/NumberWordFormatTest.java b/hutool-core/src/test/java/cn/hutool/core/convert/NumberWordFormatTest.java new file mode 100644 index 000000000..fdeb94091 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/convert/NumberWordFormatTest.java @@ -0,0 +1,18 @@ +package cn.hutool.core.convert; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.convert.NumberWordFormater; + +public class NumberWordFormatTest { + + @Test + public void formatTest() { + String format = NumberWordFormater.format(100.23); + Assert.assertEquals("ONE HUNDRED AND CENTS TWENTY THREE ONLY", format); + + String format2 = NumberWordFormater.format("2100.00"); + Assert.assertEquals("TWO THOUSAND ONE HUNDRED AND CENTS ONLY", format2); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/date/BetweenFormaterTest.java b/hutool-core/src/test/java/cn/hutool/core/date/BetweenFormaterTest.java new file mode 100644 index 000000000..33d57b673 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/date/BetweenFormaterTest.java @@ -0,0 +1,29 @@ +package cn.hutool.core.date; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.date.BetweenFormater.Level; + +public class BetweenFormaterTest { + + @Test + public void formatTest(){ + long betweenMs = DateUtil.betweenMs(DateUtil.parse("2017-01-01 22:59:59"), DateUtil.parse("2017-01-02 23:59:58")); + BetweenFormater formater = new BetweenFormater(betweenMs, Level.MILLSECOND, 1); + Assert.assertEquals(formater.toString(), "1天"); + } + + @Test + public void formatBetweenTest(){ + long betweenMs = DateUtil.betweenMs(DateUtil.parse("2018-07-16 11:23:19"), DateUtil.parse("2018-07-16 11:23:20")); + BetweenFormater formater = new BetweenFormater(betweenMs, Level.SECOND, 1); + Assert.assertEquals(formater.toString(), "1秒"); + } + + @Test + public void formatTest2(){ + BetweenFormater formater = new BetweenFormater(584, Level.SECOND, 1); + Assert.assertEquals(formater.toString(), "0秒"); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/date/DateBetweenTest.java b/hutool-core/src/test/java/cn/hutool/core/date/DateBetweenTest.java new file mode 100644 index 000000000..df21f5e04 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/date/DateBetweenTest.java @@ -0,0 +1,50 @@ +package cn.hutool.core.date; + +import java.util.Date; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.date.DateBetween; +import cn.hutool.core.date.DateUtil; + +public class DateBetweenTest { + + @Test + public void betweenYearTest() { + Date start = DateUtil.parse("2017-02-01 12:23:46"); + Date end = DateUtil.parse("2018-02-01 12:23:46"); + long betweenYear = new DateBetween(start, end).betweenYear(false); + Assert.assertEquals(1, betweenYear); + + Date start1 = DateUtil.parse("2017-02-01 12:23:46"); + Date end1 = DateUtil.parse("2018-03-01 12:23:46"); + long betweenYear1 = new DateBetween(start1, end1).betweenYear(false); + Assert.assertEquals(1, betweenYear1); + + // 不足1年 + Date start2 = DateUtil.parse("2017-02-01 12:23:46"); + Date end2 = DateUtil.parse("2018-02-01 11:23:46"); + long betweenYear2 = new DateBetween(start2, end2).betweenYear(false); + Assert.assertEquals(0, betweenYear2); + } + + @Test + public void betweenMonthTest() { + Date start = DateUtil.parse("2017-02-01 12:23:46"); + Date end = DateUtil.parse("2018-02-01 12:23:46"); + long betweenMonth = new DateBetween(start, end).betweenMonth(false); + Assert.assertEquals(12, betweenMonth); + + Date start1 = DateUtil.parse("2017-02-01 12:23:46"); + Date end1 = DateUtil.parse("2018-03-01 12:23:46"); + long betweenMonth1 = new DateBetween(start1, end1).betweenMonth(false); + Assert.assertEquals(13, betweenMonth1); + + // 不足 + Date start2 = DateUtil.parse("2017-02-01 12:23:46"); + Date end2 = DateUtil.parse("2018-02-01 11:23:46"); + long betweenMonth2 = new DateBetween(start2, end2).betweenMonth(false); + Assert.assertEquals(11, betweenMonth2); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/date/DateFieldTest.java b/hutool-core/src/test/java/cn/hutool/core/date/DateFieldTest.java new file mode 100644 index 000000000..7c258a1b0 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/date/DateFieldTest.java @@ -0,0 +1,15 @@ +package cn.hutool.core.date; + +import org.junit.Assert; +import org.junit.Test; + +public class DateFieldTest { + + @Test + public void ofTest() { + DateField field = DateField.of(11); + Assert.assertEquals(DateField.HOUR_OF_DAY, field); + field = DateField.of(12); + Assert.assertEquals(DateField.MINUTE, field); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/date/DateModifierTest.java b/hutool-core/src/test/java/cn/hutool/core/date/DateModifierTest.java new file mode 100644 index 000000000..5e700a6da --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/date/DateModifierTest.java @@ -0,0 +1,109 @@ +package cn.hutool.core.date; + +import java.util.Date; + +import org.junit.Assert; +import org.junit.Test; + +public class DateModifierTest { + + @Test + public void truncateTest() { + String dateStr = "2017-03-01 22:33:23.123"; + Date date = DateUtil.parse(dateStr); + + // 毫秒 + DateTime begin = DateUtil.truncate(date, DateField.MILLISECOND); + Assert.assertEquals(dateStr, begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN)); + + // 秒 + begin = DateUtil.truncate(date, DateField.SECOND); + Assert.assertEquals("2017-03-01 22:33:23.000", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN)); + + // 分 + begin = DateUtil.truncate(date, DateField.MINUTE); + Assert.assertEquals("2017-03-01 22:33:00.000", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN)); + + // 小时 + begin = DateUtil.truncate(date, DateField.HOUR); + Assert.assertEquals("2017-03-01 22:00:00.000", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN)); + begin = DateUtil.truncate(date, DateField.HOUR_OF_DAY); + Assert.assertEquals("2017-03-01 22:00:00.000", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN)); + + // 上下午,原始日期是22点,上下午的起始就是12点 + begin = DateUtil.truncate(date, DateField.AM_PM); + Assert.assertEquals("2017-03-01 12:00:00.000", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN)); + + // 天,day of xxx按照day处理 + begin = DateUtil.truncate(date, DateField.DAY_OF_WEEK_IN_MONTH); + Assert.assertEquals("2017-03-01 00:00:00.000", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN)); + begin = DateUtil.truncate(date, DateField.DAY_OF_WEEK); + Assert.assertEquals("2017-03-01 00:00:00.000", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN)); + begin = DateUtil.truncate(date, DateField.DAY_OF_MONTH); + Assert.assertEquals("2017-03-01 00:00:00.000", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN)); + + // 星期 + begin = DateUtil.truncate(date, DateField.WEEK_OF_MONTH); + Assert.assertEquals("2017-02-27 00:00:00.000", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN)); + begin = DateUtil.truncate(date, DateField.WEEK_OF_YEAR); + Assert.assertEquals("2017-02-27 00:00:00.000", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN)); + + // 月 + begin = DateUtil.truncate(date, DateField.MONTH); + Assert.assertEquals("2017-03-01 00:00:00.000", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN)); + + // 年 + begin = DateUtil.truncate(date, DateField.YEAR); + Assert.assertEquals("2017-01-01 00:00:00.000", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN)); + } + + @Test + public void ceilingTest() { + String dateStr = "2017-03-01 22:33:23.123"; + Date date = DateUtil.parse(dateStr); + + // 毫秒 + DateTime begin = DateUtil.ceiling(date, DateField.MILLISECOND); + Assert.assertEquals(dateStr, begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN)); + + // 秒 + begin = DateUtil.ceiling(date, DateField.SECOND); + Assert.assertEquals("2017-03-01 22:33:23.999", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN)); + + // 分 + begin = DateUtil.ceiling(date, DateField.MINUTE); + Assert.assertEquals("2017-03-01 22:33:59.999", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN)); + + // 小时 + begin = DateUtil.ceiling(date, DateField.HOUR); + Assert.assertEquals("2017-03-01 22:59:59.999", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN)); + begin = DateUtil.ceiling(date, DateField.HOUR_OF_DAY); + Assert.assertEquals("2017-03-01 22:59:59.999", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN)); + + // 上下午,原始日期是22点,上下午的结束就是23点 + begin = DateUtil.ceiling(date, DateField.AM_PM); + Assert.assertEquals("2017-03-01 23:59:59.999", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN)); + + // 天,day of xxx按照day处理 + begin = DateUtil.ceiling(date, DateField.DAY_OF_WEEK_IN_MONTH); + Assert.assertEquals("2017-03-01 23:59:59.999", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN)); + begin = DateUtil.ceiling(date, DateField.DAY_OF_WEEK); + Assert.assertEquals("2017-03-01 23:59:59.999", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN)); + begin = DateUtil.ceiling(date, DateField.DAY_OF_MONTH); + Assert.assertEquals("2017-03-01 23:59:59.999", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN)); + + // 星期 + begin = DateUtil.ceiling(date, DateField.WEEK_OF_MONTH); + Assert.assertEquals("2017-03-05 23:59:59.999", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN)); + begin = DateUtil.ceiling(date, DateField.WEEK_OF_YEAR); + Assert.assertEquals("2017-03-05 23:59:59.999", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN)); + + // 月 + begin = DateUtil.ceiling(date, DateField.MONTH); + Assert.assertEquals("2017-03-31 23:59:59.999", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN)); + + // 年 + begin = DateUtil.ceiling(date, DateField.YEAR); + Assert.assertEquals("2017-12-31 23:59:59.999", begin.toString(DatePattern.NORM_DATETIME_MS_PATTERN)); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/date/DateTimeTest.java b/hutool-core/src/test/java/cn/hutool/core/date/DateTimeTest.java new file mode 100644 index 000000000..f5f10b30d --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/date/DateTimeTest.java @@ -0,0 +1,100 @@ +package cn.hutool.core.date; + +import org.junit.Assert; +import org.junit.Test; + +/** + * DateTime单元测试 + * + * @author Looly + * + */ +public class DateTimeTest { + + @Test + public void datetimeTest() { + DateTime dateTime = new DateTime("2017-01-05 12:34:23", DatePattern.NORM_DATETIME_FORMAT); + + // 年 + int year = dateTime.year(); + Assert.assertEquals(2017, year); + + // 季度(非季节) + Quarter season = dateTime.quarterEnum(); + Assert.assertEquals(Quarter.Q1, season); + + // 月份 + Month month = dateTime.monthEnum(); + Assert.assertEquals(Month.JANUARY, month); + + // 日 + int day = dateTime.dayOfMonth(); + Assert.assertEquals(5, day); + } + + @Test + public void quarterTest() { + DateTime dateTime = new DateTime("2017-01-05 12:34:23", DatePattern.NORM_DATETIME_FORMAT); + Quarter quarter = dateTime.quarterEnum(); + Assert.assertEquals(Quarter.Q1, quarter); + + dateTime = new DateTime("2017-04-05 12:34:23", DatePattern.NORM_DATETIME_FORMAT); + quarter = dateTime.quarterEnum(); + Assert.assertEquals(Quarter.Q2, quarter); + + dateTime = new DateTime("2017-07-05 12:34:23", DatePattern.NORM_DATETIME_FORMAT); + quarter = dateTime.quarterEnum(); + Assert.assertEquals(Quarter.Q3, quarter); + + dateTime = new DateTime("2017-10-05 12:34:23", DatePattern.NORM_DATETIME_FORMAT); + quarter = dateTime.quarterEnum(); + Assert.assertEquals(Quarter.Q4, quarter); + + // 精确到毫秒 + DateTime beginTime = new DateTime("2017-10-01 00:00:00.000", DatePattern.NORM_DATETIME_MS_FORMAT); + dateTime = DateUtil.beginOfQuarter(dateTime); + Assert.assertEquals(beginTime, dateTime); + + // 精确到毫秒 + DateTime endTime = new DateTime("2017-12-31 23:59:59.999", DatePattern.NORM_DATETIME_MS_FORMAT); + dateTime = DateUtil.endOfQuarter(dateTime); + Assert.assertEquals(endTime, dateTime); + } + + @Test + public void mutableTest() { + DateTime dateTime = new DateTime("2017-01-05 12:34:23", DatePattern.NORM_DATETIME_FORMAT); + + // 默认情况下DateTime为可变对象 + DateTime offsite = dateTime.offset(DateField.YEAR, 0); + Assert.assertTrue(offsite == dateTime); + + // 设置为不可变对象后变动将返回新对象 + dateTime.setMutable(false); + offsite = dateTime.offset(DateField.YEAR, 0); + Assert.assertFalse(offsite == dateTime); + } + + @Test + public void toStringTest() { + DateTime dateTime = new DateTime("2017-01-05 12:34:23", DatePattern.NORM_DATETIME_FORMAT); + Assert.assertEquals("2017-01-05 12:34:23", dateTime.toString()); + + String dateStr = dateTime.toString("yyyy/MM/dd"); + Assert.assertEquals("2017/01/05", dateStr); + } + + @Test + public void monthTest() { + int month = DateUtil.parse("2017-07-01").month(); + Assert.assertEquals(6, month); + } + + @Test + public void weekOfYearTest() { + DateTime date = DateUtil.parse("2016-12-27"); + Assert.assertEquals(2016, date.year()); + //跨年的周返回的总是1 + Assert.assertEquals(1, date.weekOfYear()); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/date/DateUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/date/DateUtilTest.java new file mode 100644 index 000000000..9fe0e4253 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/date/DateUtilTest.java @@ -0,0 +1,563 @@ +package cn.hutool.core.date; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.TimeZone; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.BetweenFormater.Level; + +/** + * 时间工具单元测试
+ * 此单元测试依赖时区为中国+08:00 + * + *
+ * export TZ=Asia/Shanghai
+ * 
+ * + * @author Looly + * + */ +public class DateUtilTest { + + @Test + public void nowTest() { + // 当前时间 + Date date = DateUtil.date(); + Assert.assertNotNull(date); + // 当前时间 + Date date2 = DateUtil.date(Calendar.getInstance()); + Assert.assertNotNull(date2); + // 当前时间 + Date date3 = DateUtil.date(System.currentTimeMillis()); + Assert.assertNotNull(date3); + + // 当前日期字符串,格式:yyyy-MM-dd HH:mm:ss + String now = DateUtil.now(); + Assert.assertNotNull(now); + // 当前日期字符串,格式:yyyy-MM-dd + String today = DateUtil.today(); + Assert.assertNotNull(today); + } + + @Test + public void formatAndParseTest() { + String dateStr = "2017-03-01"; + Date date = DateUtil.parse(dateStr); + + String format = DateUtil.format(date, "yyyy/MM/dd"); + Assert.assertEquals("2017/03/01", format); + + // 常用格式的格式化 + String formatDate = DateUtil.formatDate(date); + Assert.assertEquals("2017-03-01", formatDate); + String formatDateTime = DateUtil.formatDateTime(date); + Assert.assertEquals("2017-03-01 00:00:00", formatDateTime); + String formatTime = DateUtil.formatTime(date); + Assert.assertEquals("00:00:00", formatTime); + } + + @Test + public void beginAndEndTest() { + String dateStr = "2017-03-01 22:33:23"; + Date date = DateUtil.parse(dateStr); + + // 一天的开始 + Date beginOfDay = DateUtil.beginOfDay(date); + Assert.assertEquals("2017-03-01 00:00:00", beginOfDay.toString()); + // 一天的结束 + Date endOfDay = DateUtil.endOfDay(date); + Assert.assertEquals("2017-03-01 23:59:59", endOfDay.toString()); + } + + @Test + public void beginAndWeedTest() { + String dateStr = "2017-03-01 22:33:23"; + DateTime date = DateUtil.parse(dateStr); + date.setFirstDayOfWeek(Week.MONDAY); + + // 一周的开始 + Date beginOfWeek = DateUtil.beginOfWeek(date); + Assert.assertEquals("2017-02-27 00:00:00", beginOfWeek.toString()); + // 一周的结束 + Date endOfWeek = DateUtil.endOfWeek(date); + Assert.assertEquals("2017-03-05 23:59:59", endOfWeek.toString()); + + Calendar calendar = DateUtil.calendar(date); + // 一周的开始 + Calendar begin = DateUtil.beginOfWeek(calendar); + Assert.assertEquals("2017-02-27 00:00:00", DateUtil.date(begin).toString()); + // 一周的结束 + Calendar end = DateUtil.endOfWeek(calendar); + Assert.assertEquals("2017-03-05 23:59:59", DateUtil.date(end).toString()); + } + + @Test + public void offsetDateTest() { + String dateStr = "2017-03-01 22:33:23"; + Date date = DateUtil.parse(dateStr); + + Date newDate = DateUtil.offset(date, DateField.DAY_OF_MONTH, 2); + Assert.assertEquals("2017-03-03 22:33:23", newDate.toString()); + + // 偏移天 + DateTime newDate2 = DateUtil.offsetDay(date, 3); + Assert.assertEquals("2017-03-04 22:33:23", newDate2.toString()); + + // 偏移小时 + DateTime newDate3 = DateUtil.offsetHour(date, -3); + Assert.assertEquals("2017-03-01 19:33:23", newDate3.toString()); + + // 偏移月 + DateTime offsetMonth = DateUtil.offsetMonth(date, -1); + Assert.assertEquals("2017-02-01 22:33:23", offsetMonth.toString()); + } + + @Test + public void offsetMonthTest() { + DateTime st = DateUtil.parseDate("2018-05-31"); + List list = new ArrayList<>(); + for (int i = 0; i < 4; i++) { + list.add(DateUtil.offsetMonth(st, i)); + } + Assert.assertEquals("2018-05-31 00:00:00", list.get(0).toString()); + Assert.assertEquals("2018-06-30 00:00:00", list.get(1).toString()); + Assert.assertEquals("2018-07-31 00:00:00", list.get(2).toString()); + Assert.assertEquals("2018-08-31 00:00:00", list.get(3).toString()); + } + + @Test + public void betweenTest() { + String dateStr1 = "2017-03-01 22:34:23"; + Date date1 = DateUtil.parse(dateStr1); + + String dateStr2 = "2017-04-01 23:56:14"; + Date date2 = DateUtil.parse(dateStr2); + + // 相差月 + long betweenMonth = DateUtil.betweenMonth(date1, date2, false); + Assert.assertEquals(1, betweenMonth);// 相差一个月 + // 反向 + betweenMonth = DateUtil.betweenMonth(date2, date1, false); + Assert.assertEquals(1, betweenMonth);// 相差一个月 + + // 相差天 + long betweenDay = DateUtil.between(date1, date2, DateUnit.DAY); + Assert.assertEquals(31, betweenDay);// 相差一个月,31天 + // 反向 + betweenDay = DateUtil.between(date2, date1, DateUnit.DAY); + Assert.assertEquals(31, betweenDay);// 相差一个月,31天 + + // 相差小时 + long betweenHour = DateUtil.between(date1, date2, DateUnit.HOUR); + Assert.assertEquals(745, betweenHour); + // 反向 + betweenHour = DateUtil.between(date2, date1, DateUnit.HOUR); + Assert.assertEquals(745, betweenHour); + + // 相差分 + long betweenMinute = DateUtil.between(date1, date2, DateUnit.MINUTE); + Assert.assertEquals(44721, betweenMinute); + // 反向 + betweenMinute = DateUtil.between(date2, date1, DateUnit.MINUTE); + Assert.assertEquals(44721, betweenMinute); + + // 相差秒 + long betweenSecond = DateUtil.between(date1, date2, DateUnit.SECOND); + Assert.assertEquals(2683311, betweenSecond); + // 反向 + betweenSecond = DateUtil.between(date2, date1, DateUnit.SECOND); + Assert.assertEquals(2683311, betweenSecond); + + // 相差秒 + long betweenMS = DateUtil.between(date1, date2, DateUnit.MS); + Assert.assertEquals(2683311000L, betweenMS); + // 反向 + betweenMS = DateUtil.between(date2, date1, DateUnit.MS); + Assert.assertEquals(2683311000L, betweenMS); + } + + @Test + public void betweenTest2() { + long between = DateUtil.between(DateUtil.parse("2019-05-06 02:15:00"), DateUtil.parse("2019-05-06 02:20:00"), DateUnit.HOUR); + Assert.assertEquals(0, between); + } + + @Test + public void formatChineseDateTest() { + String formatChineseDate = DateUtil.formatChineseDate(DateUtil.parse("2018-02-24"), true); + Assert.assertEquals("二〇一八年二月二十四日", formatChineseDate); + } + + @Test + public void formatBetweenTest() { + String dateStr1 = "2017-03-01 22:34:23"; + Date date1 = DateUtil.parse(dateStr1); + + String dateStr2 = "2017-04-01 23:56:14"; + Date date2 = DateUtil.parse(dateStr2); + + long between = DateUtil.between(date1, date2, DateUnit.MS); + String formatBetween = DateUtil.formatBetween(between, Level.MINUTE); + Assert.assertEquals("31天1小时21分", formatBetween); + } + + @Test + public void timerTest() { + TimeInterval timer = DateUtil.timer(); + + // --------------------------------- + // -------这是执行过程 + // --------------------------------- + + timer.interval();// 花费毫秒数 + timer.intervalRestart();// 返回花费时间,并重置开始时间 + timer.intervalMinute();// 花费分钟数 + } + + @Test + public void currentTest() { + long current = DateUtil.current(false); + String currentStr = String.valueOf(current); + Assert.assertEquals(13, currentStr.length()); + + long currentNano = DateUtil.current(true); + String currentNanoStr = String.valueOf(currentNano); + Assert.assertNotNull(currentNanoStr); + } + + @Test + public void weekOfYearTest() { + // 第一周周日 + int weekOfYear1 = DateUtil.weekOfYear(DateUtil.parse("2016-01-03")); + Assert.assertEquals(1, weekOfYear1); + + // 第二周周四 + int weekOfYear2 = DateUtil.weekOfYear(DateUtil.parse("2016-01-07")); + Assert.assertEquals(2, weekOfYear2); + } + + @Test + public void timeToSecondTest() { + int second = DateUtil.timeToSecond("00:01:40"); + Assert.assertEquals(100, second); + second = DateUtil.timeToSecond("00:00:40"); + Assert.assertEquals(40, second); + second = DateUtil.timeToSecond("01:00:00"); + Assert.assertEquals(3600, second); + second = DateUtil.timeToSecond("00:00:00"); + Assert.assertEquals(0, second); + } + + @Test + public void secondToTime() { + String time = DateUtil.secondToTime(3600); + Assert.assertEquals("01:00:00", time); + time = DateUtil.secondToTime(3800); + Assert.assertEquals("01:03:20", time); + time = DateUtil.secondToTime(0); + Assert.assertEquals("00:00:00", time); + time = DateUtil.secondToTime(30); + Assert.assertEquals("00:00:30", time); + } + + @Test + public void parseTest2() { + // 转换时间与SimpleDateFormat结果保持一致即可 + String birthday = "700403"; + Date birthDate = DateUtil.parse(birthday, "yyMMdd"); + // 获取出生年(完全表现形式,如:2010) + int sYear = DateUtil.year(birthDate); + Assert.assertEquals(1970, sYear); + } + + @Test + public void parseTest3() { + String dateStr = "2018-10-10 12:11:11"; + Date date = DateUtil.parse(dateStr); + String format = DateUtil.format(date, DatePattern.NORM_DATETIME_PATTERN); + Assert.assertEquals(dateStr, format); + } + + @Test + public void parseTest4() throws ParseException { + String ymd = DateUtil.parse("2019-3-21 12:20:15", "yyyy-MM-dd").toString(DatePattern.PURE_DATE_PATTERN); + Assert.assertEquals("20190321", ymd); + } + + @Test + public void parseTest5() throws ParseException { + // 测试时间解析 + String time = DateUtil.parse("22:12:12").toString(DatePattern.NORM_TIME_FORMAT); + Assert.assertEquals("22:12:12", time); + time = DateUtil.parse("2:12:12").toString(DatePattern.NORM_TIME_FORMAT); + Assert.assertEquals("02:12:12", time); + time = DateUtil.parse("2:2:12").toString(DatePattern.NORM_TIME_FORMAT); + Assert.assertEquals("02:02:12", time); + time = DateUtil.parse("2:2:1").toString(DatePattern.NORM_TIME_FORMAT); + Assert.assertEquals("02:02:01", time); + time = DateUtil.parse("22:2:1").toString(DatePattern.NORM_TIME_FORMAT); + Assert.assertEquals("22:02:01", time); + time = DateUtil.parse("2:22:1").toString(DatePattern.NORM_TIME_FORMAT); + Assert.assertEquals("02:22:01", time); + + // 测试两位时间解析 + time = DateUtil.parse("2:22").toString(DatePattern.NORM_TIME_FORMAT); + Assert.assertEquals("02:22:00", time); + time = DateUtil.parse("12:22").toString(DatePattern.NORM_TIME_FORMAT); + Assert.assertEquals("12:22:00", time); + time = DateUtil.parse("12:2").toString(DatePattern.NORM_TIME_FORMAT); + Assert.assertEquals("12:02:00", time); + + } + + @Test + public void parseTest6() throws ParseException { + String str = "Tue Jun 4 16:25:15 +0800 2019"; + DateTime dateTime = DateUtil.parse(str); + Assert.assertEquals("2019-06-04 16:25:15", dateTime.toString()); + } + + @Test + public void parseTest7() throws ParseException { + String str = "2019-06-01T19:45:43.000 +0800"; + DateTime dateTime = DateUtil.parse(str, "yyyy-MM-dd'T'HH:mm:ss.SSS Z"); + Assert.assertEquals("2019-06-01 19:45:43", dateTime.toString()); + } + + @Test + public void parseDateTest() throws ParseException { + String dateStr = "2018-4-10"; + Date date = DateUtil.parseDate(dateStr); + String format = DateUtil.format(date, DatePattern.NORM_DATE_PATTERN); + Assert.assertEquals("2018-04-10", format); + } + + @Test + public void parseToDateTimeTest1() { + String dateStr1 = "2017-02-01"; + String dateStr2 = "2017/02/01"; + String dateStr3 = "2017.02.01"; + String dateStr4 = "2017年02月01日"; + + DateTime dt1 = DateUtil.parse(dateStr1); + DateTime dt2 = DateUtil.parse(dateStr2); + DateTime dt3 = DateUtil.parse(dateStr3); + DateTime dt4 = DateUtil.parse(dateStr4); + Assert.assertEquals(dt1, dt2); + Assert.assertEquals(dt2, dt3); + Assert.assertEquals(dt3, dt4); + } + + @Test + public void parseToDateTimeTest2() { + String dateStr1 = "2017-02-01 12:23"; + String dateStr2 = "2017/02/01 12:23"; + String dateStr3 = "2017.02.01 12:23"; + String dateStr4 = "2017年02月01日 12:23"; + + DateTime dt1 = DateUtil.parse(dateStr1); + DateTime dt2 = DateUtil.parse(dateStr2); + DateTime dt3 = DateUtil.parse(dateStr3); + DateTime dt4 = DateUtil.parse(dateStr4); + Assert.assertEquals(dt1, dt2); + Assert.assertEquals(dt2, dt3); + Assert.assertEquals(dt3, dt4); + } + + @Test + public void parseToDateTimeTest3() { + String dateStr1 = "2017-02-01 12:23:45"; + String dateStr2 = "2017/02/01 12:23:45"; + String dateStr3 = "2017.02.01 12:23:45"; + String dateStr4 = "2017年02月01日 12时23分45秒"; + + DateTime dt1 = DateUtil.parse(dateStr1); + DateTime dt2 = DateUtil.parse(dateStr2); + DateTime dt3 = DateUtil.parse(dateStr3); + DateTime dt4 = DateUtil.parse(dateStr4); + Assert.assertEquals(dt1, dt2); + Assert.assertEquals(dt2, dt3); + Assert.assertEquals(dt3, dt4); + } + + @Test + public void parseToDateTimeTest4() { + String dateStr1 = "2017-02-01 12:23:45"; + String dateStr2 = "20170201122345"; + + DateTime dt1 = DateUtil.parse(dateStr1); + DateTime dt2 = DateUtil.parse(dateStr2); + Assert.assertEquals(dt1, dt2); + } + + @Test + public void parseToDateTimeTest5() { + String dateStr1 = "2017-02-01"; + String dateStr2 = "20170201"; + + DateTime dt1 = DateUtil.parse(dateStr1); + DateTime dt2 = DateUtil.parse(dateStr2); + Assert.assertEquals(dt1, dt2); + } + + @Test + public void parseUTCTest() throws ParseException { + String dateStr1 = "2018-09-13T05:34:31Z"; + DateTime dt = DateUtil.parseUTC(dateStr1); + + // parse方法支持UTC格式测试 + DateTime dt2 = DateUtil.parse(dateStr1); + Assert.assertEquals(dt, dt2); + + // 默认使用Pattern对应的时区,既UTC时区 + String dateStr = dt.toString(); + Assert.assertEquals("2018-09-13 05:34:31", dateStr); + + // 使用当前(上海)时区 + dateStr = dt.toString(TimeZone.getTimeZone("GMT+8:00")); + Assert.assertEquals("2018-09-13 13:34:31", dateStr); + + dateStr1 = "2018-09-13T13:34:32+0800"; + dt = DateUtil.parseUTC(dateStr1); + dateStr = dt.toString(TimeZone.getTimeZone("GMT+8:00")); + Assert.assertEquals("2018-09-13 13:34:32", dateStr); + + dateStr1 = "2018-09-13T13:34:33+08:00"; + dt = DateUtil.parseUTC(dateStr1); + dateStr = dt.toString(TimeZone.getTimeZone("GMT+8:00")); + Assert.assertEquals("2018-09-13 13:34:33", dateStr); + + dateStr1 = "2018-09-13T13:34:34+0800"; + dt = DateUtil.parse(dateStr1); + dateStr = dt.toString(TimeZone.getTimeZone("GMT+8:00")); + Assert.assertEquals("2018-09-13 13:34:34", dateStr); + + dateStr1 = "2018-09-13T13:34:35+08:00"; + dt = DateUtil.parse(dateStr1); + dateStr = dt.toString(TimeZone.getTimeZone("GMT+8:00")); + Assert.assertEquals("2018-09-13 13:34:35", dateStr); + + dateStr1 = "2018-09-13T13:34:36.999+0800"; + dt = DateUtil.parseUTC(dateStr1); + final SimpleDateFormat simpleDateFormat = new SimpleDateFormat(DatePattern.NORM_DATETIME_MS_PATTERN); + simpleDateFormat.setTimeZone(TimeZone.getTimeZone("GMT+8:00")); + dateStr = dt.toString(simpleDateFormat); + Assert.assertEquals("2018-09-13 13:34:36.999", dateStr); + + dateStr1 = "2018-09-13T13:34:37.999+08:00"; + dt = DateUtil.parseUTC(dateStr1); + dateStr = dt.toString(simpleDateFormat); + Assert.assertEquals("2018-09-13 13:34:37.999", dateStr); + + dateStr1 = "2018-09-13T13:34:38.999+0800"; + dt = DateUtil.parse(dateStr1); + dateStr = dt.toString(simpleDateFormat); + Assert.assertEquals("2018-09-13 13:34:38.999", dateStr); + + dateStr1 = "2018-09-13T13:34:39.999+08:00"; + dt = DateUtil.parse(dateStr1); + dateStr = dt.toString(simpleDateFormat); + Assert.assertEquals("2018-09-13 13:34:39.999", dateStr); + } + + @Test + public void parseJDkTest() throws ParseException { + String dateStr = "Thu May 16 17:57:18 GMT+08:00 2019"; + DateTime time = DateUtil.parse(dateStr); + Assert.assertEquals("2019-05-16 17:57:18", time.toString()); + } + + @Test + public void endOfWeekTest() { + DateTime now = DateUtil.date(); + + DateTime startOfWeek = DateUtil.beginOfWeek(now); + DateTime endOfWeek = DateUtil.endOfWeek(now); + + long between = DateUtil.between(endOfWeek, startOfWeek, DateUnit.DAY); + // 周一和周日相距6天 + Assert.assertEquals(6, between); + } + + @Test + public void dayOfWeekTest() { + int dayOfWeek = DateUtil.dayOfWeek(DateUtil.parse("2018-03-07")); + Assert.assertEquals(Calendar.WEDNESDAY, dayOfWeek); + Week week = DateUtil.dayOfWeekEnum(DateUtil.parse("2018-03-07")); + Assert.assertEquals(Week.WEDNESDAY, week); + } + + @Test + public void rangeTest() { + DateTime start = DateUtil.parse("2017-01-01"); + DateTime end = DateUtil.parse("2017-01-03"); + + // 测试包含开始和结束情况下步进为1的情况 + DateRange range = DateUtil.range(start, end, DateField.DAY_OF_YEAR); + Assert.assertEquals(range.next(), DateUtil.parse("2017-01-01")); + Assert.assertEquals(range.next(), DateUtil.parse("2017-01-02")); + Assert.assertEquals(range.next(), DateUtil.parse("2017-01-03")); + try { + range.next(); + Assert.fail("已超过边界,下一个元素不应该存在!"); + } catch (NoSuchElementException e) { + } + + // 测试多步进的情况 + range = new DateRange(start, end, DateField.DAY_OF_YEAR, 2); + Assert.assertEquals(range.next(), DateUtil.parse("2017-01-01")); + Assert.assertEquals(range.next(), DateUtil.parse("2017-01-03")); + + // 测试不包含开始结束时间的情况 + range = new DateRange(start, end, DateField.DAY_OF_YEAR, 1, false, false); + Assert.assertEquals(range.next(), DateUtil.parse("2017-01-02")); + try { + range.next(); + Assert.fail("不包含结束时间情况下,下一个元素不应该存在!"); + } catch (NoSuchElementException e) { + } + } + + @Test + public void rangeToListTest() { + DateTime start = DateUtil.parse("2017-01-01"); + DateTime end = DateUtil.parse("2017-01-31"); + + List rangeToList = DateUtil.rangeToList(start, end, DateField.DAY_OF_YEAR); + Assert.assertEquals(rangeToList.get(0), DateUtil.parse("2017-01-01")); + Assert.assertEquals(rangeToList.get(1), DateUtil.parse("2017-01-02")); + } + + @Test + public void yearAndQTest() { + String yearAndQuarter = DateUtil.yearAndQuarter(DateUtil.parse("2018-12-01")); + Assert.assertEquals("20184", yearAndQuarter); + + LinkedHashSet yearAndQuarters = DateUtil.yearAndQuarter(DateUtil.parse("2018-09-10"), DateUtil.parse("2018-12-20")); + List list = CollUtil.list(false, yearAndQuarters); + Assert.assertEquals(2, list.size()); + Assert.assertEquals("20183", list.get(0)); + Assert.assertEquals("20184", list.get(1)); + + LinkedHashSet yearAndQuarters2 = DateUtil.yearAndQuarter(DateUtil.parse("2018-10-10"), DateUtil.parse("2018-12-10")); + List list2 = CollUtil.list(false, yearAndQuarters2); + Assert.assertEquals(1, list2.size()); + Assert.assertEquals("20184", list2.get(0)); + } + + @Test + public void formatHttpDateTest() { + String formatHttpDate = DateUtil.formatHttpDate(DateUtil.parse("2019-01-02 22:32:01")); + Assert.assertEquals("Wed, 02 Jan 2019 14:32:01 GMT", formatHttpDate); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/date/TimeZoneTest.java b/hutool-core/src/test/java/cn/hutool/core/date/TimeZoneTest.java new file mode 100644 index 000000000..98a9b3bca --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/date/TimeZoneTest.java @@ -0,0 +1,23 @@ +package cn.hutool.core.date; + +import java.util.TimeZone; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.date.format.FastDateFormat; + +public class TimeZoneTest { + + @Test + public void timeZoneConvertTest() { + DateTime dt = DateUtil.parse("2018-07-10 21:44:32", // + FastDateFormat.getInstance(DatePattern.NORM_DATETIME_PATTERN, TimeZone.getTimeZone("GMT+8:00"))); + Assert.assertEquals("2018-07-10 21:44:32", dt.toString()); + + dt.setTimeZone(TimeZone.getTimeZone("Europe/London")); + int hour = dt.getField(DateField.HOUR_OF_DAY); + Assert.assertEquals(14, hour); + Assert.assertEquals("2018-07-10 14:44:32", dt.toString()); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/date/ZodiacTest.java b/hutool-core/src/test/java/cn/hutool/core/date/ZodiacTest.java new file mode 100644 index 000000000..986e490d5 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/date/ZodiacTest.java @@ -0,0 +1,21 @@ +package cn.hutool.core.date; + +import org.junit.Assert; +import org.junit.Test; + +public class ZodiacTest { + + @Test + public void getZodiacTest() { + Assert.assertEquals("摩羯座", Zodiac.getZodiac(Month.JANUARY, 19)); + Assert.assertEquals("水瓶座", Zodiac.getZodiac(Month.JANUARY, 20)); + Assert.assertEquals("巨蟹座", Zodiac.getZodiac(6, 17)); + } + + @Test + public void getChineseZodiacTest() { + Assert.assertEquals("狗", Zodiac.getChineseZodiac(1994)); + Assert.assertEquals("狗", Zodiac.getChineseZodiac(2018)); + Assert.assertEquals("猪", Zodiac.getChineseZodiac(2019)); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/exceptions/ExceptionUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/exceptions/ExceptionUtilTest.java new file mode 100644 index 000000000..9bcbba672 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/exceptions/ExceptionUtilTest.java @@ -0,0 +1,37 @@ +package cn.hutool.core.exceptions; + +import cn.hutool.core.io.IORuntimeException; +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; + +/** + * 异常工具单元测试 + * + * @author looly + */ +public class ExceptionUtilTest { + + @Test + public void wrapTest() { + IORuntimeException e = ExceptionUtil.wrap(new IOException(), IORuntimeException.class); + Assert.assertNotNull(e); + } + + @Test + public void getRootTest() { + // 查找入口方法 + StackTraceElement ele = ExceptionUtil.getRootStackElement(); + Assert.assertEquals("main", ele.getMethodName()); + } + + @Test + public void convertTest() { + // RuntimeException e = new RuntimeException(); + IOException ioException = new IOException(); + IllegalArgumentException argumentException = new IllegalArgumentException(ioException); + IOException ioException1 = ExceptionUtil.convertFromOrSuppressedThrowable(argumentException, IOException.class, true); + Assert.assertNotNull(ioException1); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/img/ImgTest.java b/hutool-core/src/test/java/cn/hutool/core/img/ImgTest.java new file mode 100644 index 000000000..e37611198 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/img/ImgTest.java @@ -0,0 +1,27 @@ +package cn.hutool.core.img; + +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.io.FileUtil; + +public class ImgTest { + + @Test + @Ignore + public void cutTest1() { + Img.from(FileUtil.file("e:/pic/face.jpg")).cut(0, 0, 200).write(FileUtil.file("e:/pic/face_radis.png")); + } + + @Test + @Ignore + public void compressTest() { + Img.from(FileUtil.file("f:/test/4347273249269e3fb272341acc42d4e.jpg")).setQuality(0.8).write(FileUtil.file("f:/test/test_dest.jpg")); + } + + @Test + @Ignore + public void roundTest() { + Img.from(FileUtil.file("e:/pic/face.jpg")).round(0.5).write(FileUtil.file("e:/pic/face_round.png")); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/img/ImgUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/img/ImgUtilTest.java new file mode 100644 index 000000000..64a618b56 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/img/ImgUtilTest.java @@ -0,0 +1,105 @@ +package cn.hutool.core.img; + +import java.awt.Color; +import java.awt.Font; +import java.awt.Image; +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import java.io.IOException; + +import javax.imageio.ImageIO; + +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.io.FileUtil; + +public class ImgUtilTest { + + @Test + @Ignore + public void scaleTest() { + ImgUtil.scale(FileUtil.file("e:/pic/test.jpg"), FileUtil.file("e:/pic/test_result.jpg"), 0.8f); + } + + @Test + @Ignore + public void scalePngTest() { + ImgUtil.scale(FileUtil.file("f:/test/test.png"), FileUtil.file("f:/test/test_result.png"), 0.5f); + } + + @Test + @Ignore + public void scaleByWidthAndHeightTest() { + ImgUtil.scale(FileUtil.file("f:/test/aaa.jpg"), FileUtil.file("f:/test/aaa_result.jpg"), 100, 400, Color.BLUE); + } + + @Test + @Ignore + public void cutTest() { + ImgUtil.cut(FileUtil.file("d:/face.jpg"), FileUtil.file("d:/face_result.jpg"), new Rectangle(200, 200, 100, 100)); + } + + @Test + @Ignore + public void rotateTest() throws IOException { + Image image = ImgUtil.rotate(ImageIO.read(FileUtil.file("e:/pic/366466.jpg")), 180); + ImgUtil.write(image, FileUtil.file("e:/pic/result.png")); + } + + @Test + @Ignore + public void flipTest() throws IOException { + ImgUtil.flip(FileUtil.file("d:/logo.png"), FileUtil.file("d:/result.png")); + } + + @Test + @Ignore + public void pressImgTest() { + ImgUtil.pressImage(FileUtil.file("d:/picTest/1.jpg"), FileUtil.file("d:/picTest/dest.jpg"), ImgUtil.read(FileUtil.file("d:/picTest/1432613.jpg")), 0, 0, 0.1f); + } + + @Test + @Ignore + public void pressTextTest() { + ImgUtil.pressText(// + FileUtil.file("e:/pic/face.jpg"), // + FileUtil.file("e:/pic/test2_result.png"), // + "版权所有", Color.WHITE, // + new Font("黑体", Font.BOLD, 100), // + 0, // + 0, // + 0.8f); + } + + @Test + @Ignore + public void sliceByRowsAndColsTest() { + ImgUtil.sliceByRowsAndCols(FileUtil.file("e:/pic/1.png"), FileUtil.file("e:/pic/dest"), 10, 10); + } + + @Test + @Ignore + public void convertTest() { + ImgUtil.convert(FileUtil.file("e:/test2.png"), FileUtil.file("e:/test2Convert.jpg")); + } + + @Test + @Ignore + public void writeTest() { + ImgUtil.write(ImgUtil.read("e:/test2.png"), FileUtil.file("e:/test2Write.jpg")); + } + + @Test + @Ignore + public void compressTest() { + ImgUtil.compress(FileUtil.file("e:/pic/1111.png"), FileUtil.file("e:/pic/1111_target.jpg"), 0.8f); + } + + @Test + @Ignore + public void copyTest() { + BufferedImage image = ImgUtil.copyImage(ImgUtil.read("f:/pic/test.png"), BufferedImage.TYPE_INT_RGB); + ImgUtil.write(image, FileUtil.file("f:/pic/test_dest.jpg")); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/io/BufferUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/io/BufferUtilTest.java new file mode 100644 index 000000000..64bb74d00 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/io/BufferUtilTest.java @@ -0,0 +1,66 @@ +package cn.hutool.core.io; + +import java.nio.ByteBuffer; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; + +/** + * BufferUtil单元测试 + * + * @author looly + * + */ +public class BufferUtilTest { + + @Test + public void copyTest() { + byte[] bytes = "AAABBB".getBytes(); + ByteBuffer buffer = ByteBuffer.wrap(bytes); + + ByteBuffer buffer2 = BufferUtil.copy(buffer, ByteBuffer.allocate(5)); + Assert.assertEquals("AAABB", StrUtil.utf8Str(buffer2)); + } + + @Test + public void readBytesTest() { + byte[] bytes = "AAABBB".getBytes(); + ByteBuffer buffer = ByteBuffer.wrap(bytes); + + byte[] bs = BufferUtil.readBytes(buffer, 5); + Assert.assertEquals("AAABB", StrUtil.utf8Str(bs)); + } + + @Test + public void readBytes2Test() { + byte[] bytes = "AAABBB".getBytes(); + ByteBuffer buffer = ByteBuffer.wrap(bytes); + + byte[] bs = BufferUtil.readBytes(buffer, 5); + Assert.assertEquals("AAABB", StrUtil.utf8Str(bs)); + } + + @Test + public void readLineTest() { + String text = "aa\r\nbbb\ncc"; + ByteBuffer buffer = ByteBuffer.wrap(text.getBytes()); + + // 第一行 + String line = BufferUtil.readLine(buffer, CharsetUtil.CHARSET_UTF_8); + Assert.assertEquals("aa", line); + + // 第二行 + line = BufferUtil.readLine(buffer, CharsetUtil.CHARSET_UTF_8); + Assert.assertEquals("bbb", line); + + // 第三行因为没有行结束标志,因此返回null + line = BufferUtil.readLine(buffer, CharsetUtil.CHARSET_UTF_8); + Assert.assertNull(line); + + // 读取剩余部分 + Assert.assertEquals("cc", StrUtil.utf8Str(BufferUtil.readBytes(buffer))); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/io/ClassPathResourceTest.java b/hutool-core/src/test/java/cn/hutool/core/io/ClassPathResourceTest.java new file mode 100644 index 000000000..94a2c9496 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/io/ClassPathResourceTest.java @@ -0,0 +1,62 @@ +package cn.hutool.core.io; + +import java.io.IOException; +import java.util.Properties; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.io.resource.ClassPathResource; +import cn.hutool.core.util.StrUtil; + +/** + * ClassPath资源读取测试 + * @author Looly + * + */ +public class ClassPathResourceTest { + + @Test + public void readStringTest() throws IOException{ + ClassPathResource resource = new ClassPathResource("test.properties"); + String content = resource.readUtf8Str(); + Assert.assertTrue(StrUtil.isNotEmpty(content)); + } + + @Test + public void readStringTest2() throws IOException{ + ClassPathResource resource = new ClassPathResource("/"); + String content = resource.readUtf8Str(); + Assert.assertTrue(StrUtil.isNotEmpty(content)); + } + + @Test + public void readTest() throws IOException{ + ClassPathResource resource = new ClassPathResource("test.properties"); + Properties properties = new Properties(); + properties.load(resource.getStream()); + + Assert.assertEquals("1", properties.get("a")); + Assert.assertEquals("2", properties.get("b")); + } + + @Test + public void readFromJarTest() throws IOException{ + //测试读取junit的jar包下的LICENSE-junit.txt文件 + final ClassPathResource resource = new ClassPathResource("LICENSE-junit.txt"); + + String result = resource.readUtf8Str(); + Assert.assertNotNull(result); + + //二次读取测试,用于测试关闭流对再次读取的影响 + result = resource.readUtf8Str(); + Assert.assertNotNull(result); + } + + @Test + public void getAbsTest() { + final ClassPathResource resource = new ClassPathResource("LICENSE-junit.txt"); + String absPath = resource.getAbsolutePath(); + Assert.assertTrue(absPath.contains("LICENSE-junit.txt")); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/io/FileCopierTest.java b/hutool-core/src/test/java/cn/hutool/core/io/FileCopierTest.java new file mode 100644 index 000000000..5bc60d208 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/io/FileCopierTest.java @@ -0,0 +1,45 @@ +package cn.hutool.core.io; + +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.io.file.FileCopier; + +/** + * 文件拷贝单元测试 + * @author Looly + * + */ +public class FileCopierTest { + + @Test + @Ignore + public void dirCopyTest() { + FileCopier copier = FileCopier.create("D:\\Java", "e:/eclipse/eclipse2.zip"); + copier.copy(); + } + + @Test + @Ignore + public void dirCopyTest2() { + //测试带.的文件夹复制 + FileCopier copier = FileCopier.create("D:\\workspace\\java\\.metadata", "D:\\workspace\\java\\.metadata\\temp"); + copier.copy(); + + FileUtil.copy("D:\\workspace\\java\\looly\\hutool\\.git", "D:\\workspace\\java\\temp", true); + } + + @Test(expected = IORuntimeException.class) + public void dirCopySubTest() { + //测试父目录复制到子目录报错 + FileCopier copier = FileCopier.create("D:\\workspace\\java\\.metadata", "D:\\workspace\\java\\.metadata\\temp"); + copier.copy(); + } + + @Test + @Ignore + public void copyFileToDirTest() { + FileCopier copier = FileCopier.create("d:/GReen_Soft/XshellXftpPortable.zip", "c:/hp/"); + copier.copy(); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/io/FileReaderTest.java b/hutool-core/src/test/java/cn/hutool/core/io/FileReaderTest.java new file mode 100644 index 000000000..de79ea732 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/io/FileReaderTest.java @@ -0,0 +1,21 @@ +package cn.hutool.core.io; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.io.file.FileReader; + +/** + * 文件读取测试 + * @author Looly + * + */ +public class FileReaderTest { + + @Test + public void fileReaderTest(){ + FileReader fileReader = new FileReader("test.properties"); + String result = fileReader.readString(); + Assert.assertNotNull(result); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/io/FileTypeUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/io/FileTypeUtilTest.java new file mode 100644 index 000000000..4ff554ab0 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/io/FileTypeUtilTest.java @@ -0,0 +1,39 @@ +package cn.hutool.core.io; + +import java.io.File; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.io.FileTypeUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.lang.Console; + +/** + * 文件类型判断单元测试 + * @author Looly + * + */ +public class FileTypeUtilTest { + + @Test + @Ignore + public void fileTypeUtilTest() { + File file = FileUtil.file("hutool.jpg"); + String type = FileTypeUtil.getType(file); + Assert.assertEquals("jpg", type); + + FileTypeUtil.putFileType("ffd8ffe000104a464946", "new_jpg"); + String newType = FileTypeUtil.getType(file); + Assert.assertEquals("new_jpg", newType); + } + + @Test + @Ignore + public void emptyTest() { + File file = FileUtil.file("d:/empty.txt"); + String type = FileTypeUtil.getType(file); + Console.log(type); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/io/FileUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/io/FileUtilTest.java new file mode 100644 index 000000000..197f6213a --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/io/FileUtilTest.java @@ -0,0 +1,323 @@ +package cn.hutool.core.io; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.io.file.LineSeparator; +import cn.hutool.core.lang.Console; +import cn.hutool.core.util.CharsetUtil; + +/** + * {@link FileUtil} 单元测试类 + * + * @author Looly + */ +public class FileUtilTest { + + @Test(expected = IllegalArgumentException.class) + public void fileTest() { + File file = FileUtil.file("d:/aaa", "bbb"); + Assert.assertNotNull(file); + + // 构建目录中出现非子目录抛出异常 + FileUtil.file(file, "../ccc"); + + FileUtil.file("E:/"); + } + + @Test + public void getAbsolutePathTest() { + String absolutePath = FileUtil.getAbsolutePath("LICENSE-junit.txt"); + Assert.assertNotNull(absolutePath); + String absolutePath2 = FileUtil.getAbsolutePath(absolutePath); + Assert.assertNotNull(absolutePath2); + Assert.assertEquals(absolutePath, absolutePath2); + } + + @Test + public void getAbsolutePathTest2() { + String path = FileUtil.getAbsolutePath("中文.xml"); + Assert.assertTrue(path.contains("中文.xml")); + } + + @Test + @Ignore + public void touchTest() { + FileUtil.touch("d:\\tea\\a.jpg"); + } + + @Test + @Ignore + public void delTest() { + // 删除一个不存在的文件,应返回true + boolean result = FileUtil.del("e:/Hutool_test_3434543533409843.txt"); + Assert.assertTrue(result); + } + + @Test + @Ignore + public void delTest2() { + // 删除一个不存在的文件,应返回true + boolean result = FileUtil.del(Paths.get("e:/Hutool_test_3434543533409843.txt")); + Assert.assertTrue(result); + } + + @Test + @Ignore + public void renameTest() { + FileUtil.rename(FileUtil.file("hutool.jpg"), "b.png", false, false); + } + + @Test + public void copyTest() throws Exception { + File srcFile = FileUtil.file("hutool.jpg"); + File destFile = FileUtil.file("hutool.copy.jpg"); + + FileUtil.copy(srcFile, destFile, true); + + Assert.assertTrue(destFile.exists()); + Assert.assertEquals(srcFile.length(), destFile.length()); + } + + @Test + @Ignore + public void copyFilesFromDir() throws Exception { + File srcFile = FileUtil.file("D:\\驱动"); + File destFile = FileUtil.file("d:\\驱动备份"); + + FileUtil.copyFilesFromDir(srcFile, destFile, true); + } + + @Test + public void equlasTest() { + // 源文件和目标文件都不存在 + File srcFile = FileUtil.file("d:/hutool.jpg"); + File destFile = FileUtil.file("d:/hutool.jpg"); + + boolean equals = FileUtil.equals(srcFile, destFile); + Assert.assertTrue(equals); + + // 源文件存在,目标文件不存在 + File srcFile1 = FileUtil.file("hutool.jpg"); + File destFile1 = FileUtil.file("d:/hutool.jpg"); + + boolean notEquals = FileUtil.equals(srcFile1, destFile1); + Assert.assertFalse(notEquals); + } + + @Test + @Ignore + public void convertLineSeparatorTest() { + FileUtil.convertLineSeparator(FileUtil.file("d:/aaa.txt"), CharsetUtil.CHARSET_UTF_8, LineSeparator.WINDOWS); + } + + @Test + public void normalizeTest() { + Assert.assertEquals("/foo/", FileUtil.normalize("/foo//")); + Assert.assertEquals("/foo/", FileUtil.normalize("/foo/./")); + Assert.assertEquals("/bar", FileUtil.normalize("/foo/../bar")); + Assert.assertEquals("/bar/", FileUtil.normalize("/foo/../bar/")); + Assert.assertEquals("/baz", FileUtil.normalize("/foo/../bar/../baz")); + Assert.assertEquals("/", FileUtil.normalize("/../")); + Assert.assertEquals("foo", FileUtil.normalize("foo/bar/..")); + Assert.assertEquals("bar", FileUtil.normalize("foo/../../bar")); + Assert.assertEquals("bar", FileUtil.normalize("foo/../bar")); + Assert.assertEquals("/server/bar", FileUtil.normalize("//server/foo/../bar")); + Assert.assertEquals("/bar", FileUtil.normalize("//server/../bar")); + Assert.assertEquals("C:/bar", FileUtil.normalize("C:\\foo\\..\\bar")); + Assert.assertEquals("C:/bar", FileUtil.normalize("C:\\..\\bar")); + Assert.assertEquals("~/bar/", FileUtil.normalize("~/foo/../bar/")); + Assert.assertEquals("bar", FileUtil.normalize("~/../bar")); + Assert.assertEquals("bar", FileUtil.normalize("../../bar")); + Assert.assertEquals("C:/bar", FileUtil.normalize("/C:/bar")); + } + + @Test + public void normalizeClassPathTest() { + Assert.assertEquals("", FileUtil.normalize("classpath:")); + } + + @Test + public void doubleNormalizeTest() { + String normalize = FileUtil.normalize("/aa/b:/c"); + String normalize2 = FileUtil.normalize(normalize); + Assert.assertEquals("/aa/b:/c", normalize); + Assert.assertEquals(normalize, normalize2); + } + + @Test + public void subPathTest() { + Path path = Paths.get("/aaa/bbb/ccc/ddd/eee/fff"); + + Path subPath = FileUtil.subPath(path, 5, 4); + Assert.assertEquals("eee", subPath.toString()); + subPath = FileUtil.subPath(path, 0, 1); + Assert.assertEquals("aaa", subPath.toString()); + subPath = FileUtil.subPath(path, 1, 0); + Assert.assertEquals("aaa", subPath.toString()); + + // 负数 + subPath = FileUtil.subPath(path, -1, 0); + Assert.assertEquals("aaa/bbb/ccc/ddd/eee", subPath.toString().replace('\\', '/')); + subPath = FileUtil.subPath(path, -1, Integer.MAX_VALUE); + Assert.assertEquals("fff", subPath.toString()); + subPath = FileUtil.subPath(path, -1, path.getNameCount()); + Assert.assertEquals("fff", subPath.toString()); + subPath = FileUtil.subPath(path, -2, -3); + Assert.assertEquals("ddd", subPath.toString()); + } + + @Test + public void subPathTest2() { + String subPath = FileUtil.subPath("d:/aaa/bbb/", "d:/aaa/bbb/ccc/"); + Assert.assertEquals("ccc/", subPath); + + subPath = FileUtil.subPath("d:/aaa/bbb", "d:/aaa/bbb/ccc/"); + Assert.assertEquals("ccc/", subPath); + + subPath = FileUtil.subPath("d:/aaa/bbb", "d:/aaa/bbb/ccc/test.txt"); + Assert.assertEquals("ccc/test.txt", subPath); + + subPath = FileUtil.subPath("d:/aaa/bbb/", "d:/aaa/bbb/ccc"); + Assert.assertEquals("ccc", subPath); + + subPath = FileUtil.subPath("d:/aaa/bbb", "d:/aaa/bbb/ccc"); + Assert.assertEquals("ccc", subPath); + + subPath = FileUtil.subPath("d:/aaa/bbb", "d:/aaa/bbb"); + Assert.assertEquals("", subPath); + + subPath = FileUtil.subPath("d:/aaa/bbb/", "d:/aaa/bbb"); + Assert.assertEquals("", subPath); + } + + @Test + public void getPathEle() { + Path path = Paths.get("/aaa/bbb/ccc/ddd/eee/fff"); + + Path ele = FileUtil.getPathEle(path, -1); + Assert.assertEquals("fff", ele.toString()); + ele = FileUtil.getPathEle(path, 0); + Assert.assertEquals("aaa", ele.toString()); + ele = FileUtil.getPathEle(path, -5); + Assert.assertEquals("bbb", ele.toString()); + ele = FileUtil.getPathEle(path, -6); + Assert.assertEquals("aaa", ele.toString()); + } + + @Test + public void listFileNamesTest() { + List names = FileUtil.listFileNames("classpath:"); + Assert.assertTrue(names.contains("hutool.jpg")); + + names = FileUtil.listFileNames(""); + Assert.assertTrue(names.contains("hutool.jpg")); + + names = FileUtil.listFileNames("."); + Assert.assertTrue(names.contains("hutool.jpg")); + } + + @Test + @Ignore + public void loopFilesTest() { + List files = FileUtil.loopFiles("d:/"); + for (File file : files) { + Console.log(file.getPath()); + } + } + + @Test + public void getParentTest() { + // 只在Windows下测试 + if (FileUtil.isWindows()) { + File parent = FileUtil.getParent(FileUtil.file("d:/aaa/bbb/cc/ddd"), 0); + Assert.assertEquals(FileUtil.file("d:\\aaa\\bbb\\cc\\ddd"), parent); + + parent = FileUtil.getParent(FileUtil.file("d:/aaa/bbb/cc/ddd"), 1); + Assert.assertEquals(FileUtil.file("d:\\aaa\\bbb\\cc"), parent); + + parent = FileUtil.getParent(FileUtil.file("d:/aaa/bbb/cc/ddd"), 2); + Assert.assertEquals(FileUtil.file("d:\\aaa\\bbb"), parent); + + parent = FileUtil.getParent(FileUtil.file("d:/aaa/bbb/cc/ddd"), 4); + Assert.assertEquals(FileUtil.file("d:\\"), parent); + + parent = FileUtil.getParent(FileUtil.file("d:/aaa/bbb/cc/ddd"), 5); + Assert.assertNull(parent); + + parent = FileUtil.getParent(FileUtil.file("d:/aaa/bbb/cc/ddd"), 10); + Assert.assertNull(parent); + } + } + + @Test + public void lastIndexOfSeparatorTest() { + String dir = "d:\\aaa\\bbb\\cc\\ddd"; + int index = FileUtil.lastIndexOfSeparator(dir); + Assert.assertEquals(13, index); + + String file = "ddd.jpg"; + int index2 = FileUtil.lastIndexOfSeparator(file); + Assert.assertEquals(-1, index2); + } + + @Test + public void getNameTest() { + String path = "d:\\aaa\\bbb\\cc\\ddd\\"; + String name = FileUtil.getName(path); + Assert.assertEquals("ddd", name); + + path = "d:\\aaa\\bbb\\cc\\ddd.jpg"; + name = FileUtil.getName(path); + Assert.assertEquals("ddd.jpg", name); + } + + @Test + public void mainNameTest() { + String path = "d:\\aaa\\bbb\\cc\\ddd\\"; + String mainName = FileUtil.mainName(path); + Assert.assertEquals("ddd", mainName); + + path = "d:\\aaa\\bbb\\cc\\ddd"; + mainName = FileUtil.mainName(path); + Assert.assertEquals("ddd", mainName); + + path = "d:\\aaa\\bbb\\cc\\ddd.jpg"; + mainName = FileUtil.mainName(path); + Assert.assertEquals("ddd", mainName); + } + + @Test + public void extNameTest() { + String path = "d:\\aaa\\bbb\\cc\\ddd\\"; + String mainName = FileUtil.extName(path); + Assert.assertEquals("", mainName); + + path = "d:\\aaa\\bbb\\cc\\ddd"; + mainName = FileUtil.extName(path); + Assert.assertEquals("", mainName); + + path = "d:\\aaa\\bbb\\cc\\ddd.jpg"; + mainName = FileUtil.extName(path); + Assert.assertEquals("jpg", mainName); + } + + @Test + public void getWebRootTest() { + File webRoot = FileUtil.getWebRoot(); + Assert.assertNotNull(webRoot); + Assert.assertEquals("hutool-core", webRoot.getName()); + } + + @Test + public void getMimeTypeTest() { + String mimeType = FileUtil.getMimeType("test2Write.jpg"); + Assert.assertEquals("image/jpeg", mimeType); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/io/WatchMonitorTest.java b/hutool-core/src/test/java/cn/hutool/core/io/WatchMonitorTest.java new file mode 100644 index 000000000..d7e7ced6f --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/io/WatchMonitorTest.java @@ -0,0 +1,54 @@ +package cn.hutool.core.io; + +import java.nio.file.Path; +import java.nio.file.WatchEvent; + +import cn.hutool.core.io.watch.SimpleWatcher; +import cn.hutool.core.io.watch.WatchMonitor; +import cn.hutool.core.io.watch.Watcher; +import cn.hutool.core.io.watch.watchers.DelayWatcher; +import cn.hutool.core.lang.Console; + +/** + * 文件监听单元测试 + * + * @author Looly + * + */ +public class WatchMonitorTest { + + public static void main(String[] args) { + Watcher watcher = new SimpleWatcher(){ + @Override + public void onCreate(WatchEvent event, Path currentPath) { + Object obj = event.context(); + Console.log("创建:{}-> {}", currentPath, obj); + } + + @Override + public void onModify(WatchEvent event, Path currentPath) { + Object obj = event.context(); + Console.log("修改:{}-> {}", currentPath, obj); + } + + @Override + public void onDelete(WatchEvent event, Path currentPath) { + Object obj = event.context(); + Console.log("删除:{}-> {}", currentPath, obj); + } + + @Override + public void onOverflow(WatchEvent event, Path currentPath) { + Object obj = event.context(); + Console.log("Overflow:{}-> {}", currentPath, obj); + } + }; + + WatchMonitor monitor = WatchMonitor.createAll("d:/aaa/aaa.txt", new DelayWatcher(watcher, 500)); + + monitor.setMaxDepth(0); + monitor.start(); + } + + +} diff --git a/hutool-core/src/test/java/cn/hutool/core/io/checksum/CrcTest.java b/hutool-core/src/test/java/cn/hutool/core/io/checksum/CrcTest.java new file mode 100644 index 000000000..8a20602f5 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/io/checksum/CrcTest.java @@ -0,0 +1,35 @@ +package cn.hutool.core.io.checksum; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.util.HexUtil; + +/** + * CRC校验单元测试 + * + * @author looly + * + */ +public class CrcTest { + + @Test + public void crc8Test() { + final int CRC_POLYNOM = 0x9C; + final byte CRC_INITIAL = (byte) 0xFF; + + final byte[] data = { 1, 56, -23, 3, 0, 19, 0, 0, 2, 0, 3, 13, 8, -34, 7, 9, 42, 18, 26, -5, 54, 11, -94, // + -46, -128, 4, 48, 52, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 1, -32, -80, 0, 98, -5, 71, 0, 64, 0, 0, 0, 0, -116, 1, 104, 2 }; + CRC8 crc8 = new CRC8(CRC_POLYNOM, CRC_INITIAL); + crc8.update(data, 0, data.length); + Assert.assertEquals(29, crc8.getValue()); + } + + @Test + public void crc16Test() { + CRC16 crc = new CRC16(); + crc.update(12); + crc.update(16); + Assert.assertEquals("cc04", HexUtil.toHex(crc.getValue())); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/io/file/TailerTest.java b/hutool-core/src/test/java/cn/hutool/core/io/file/TailerTest.java new file mode 100644 index 000000000..50263d484 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/io/file/TailerTest.java @@ -0,0 +1,23 @@ +package cn.hutool.core.io.file; + +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.CharsetUtil; + +public class TailerTest { + + @Test + @Ignore + public void tailTest() { + FileUtil.tail(FileUtil.file("e:/tail.txt"), CharsetUtil.CHARSET_GBK); + } + + @Test + @Ignore + public void tailWithLinesTest() { + Tailer tailer = new Tailer(FileUtil.file("f:/test/test.log"), Tailer.CONSOLE_HANDLER, 2); + tailer.start(); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/lang/AssertTest.java b/hutool-core/src/test/java/cn/hutool/core/lang/AssertTest.java new file mode 100644 index 000000000..d7136f271 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/lang/AssertTest.java @@ -0,0 +1,17 @@ +package cn.hutool.core.lang; + +import org.junit.Test; + +public class AssertTest { + + @Test + public void isNullTest(){ + String a = null; + cn.hutool.core.lang.Assert.isNull(a); + } + @Test + public void notNullTest(){ + String a = null; + cn.hutool.core.lang.Assert.isNull(a); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/lang/CallerTest.java b/hutool-core/src/test/java/cn/hutool/core/lang/CallerTest.java new file mode 100644 index 000000000..1f2e51f51 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/lang/CallerTest.java @@ -0,0 +1,38 @@ +package cn.hutool.core.lang; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.lang.caller.CallerUtil; + +/** + * {@link CallerUtil} 单元测试 + * @author Looly + * + */ +public class CallerTest { + + @Test + public void getCallerTest() { + Class caller = CallerUtil.getCaller(); + Assert.assertEquals(this.getClass(), caller); + + Class caller0 = CallerUtil.getCaller(0); + Assert.assertEquals(CallerUtil.class, caller0); + + Class caller1 = CallerUtil.getCaller(1); + Assert.assertEquals(this.getClass(), caller1); + } + + @Test + public void getCallerCallerTest() { + Class callerCaller = CallerTestClass.getCaller(); + Assert.assertEquals(this.getClass(), callerCaller); + } + + private static class CallerTestClass{ + public static Class getCaller(){ + return CallerUtil.getCallerCaller(); + } + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/lang/ClassScanerTest.java b/hutool-core/src/test/java/cn/hutool/core/lang/ClassScanerTest.java new file mode 100644 index 000000000..2d1249ca0 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/lang/ClassScanerTest.java @@ -0,0 +1,19 @@ +package cn.hutool.core.lang; + +import java.util.Set; + +import org.junit.Ignore; +import org.junit.Test; + +public class ClassScanerTest { + + @Test + @Ignore + public void scanTest() { + ClassScaner scaner = new ClassScaner("cn.hutool.core.util.StrUtil", null); + Set> set = scaner.scan(); + for (Class clazz : set) { + Console.log(clazz.getName()); + } + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/lang/ConsoleTest.java b/hutool-core/src/test/java/cn/hutool/core/lang/ConsoleTest.java new file mode 100644 index 000000000..8eb8542a3 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/lang/ConsoleTest.java @@ -0,0 +1,60 @@ +package cn.hutool.core.lang; + +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.lang.Console; +import cn.hutool.core.thread.ThreadUtil; + +/** + * 控制台单元测试 + * @author Looly + * + */ +public class ConsoleTest { + + @Test + public void logTest(){ + Console.log(); + + String[] a = {"abc", "bcd", "def"}; + Console.log(a); + + Console.log("This is Console log for {}.", "test"); + } + + @Test + public void printTest(){ + String[] a = {"abc", "bcd", "def"}; + Console.print(a); + + Console.log("This is Console print for {}.", "test"); + } + + @Test + public void errorTest(){ + Console.error(); + + String[] a = {"abc", "bcd", "def"}; + Console.error(a); + + Console.error("This is Console error for {}.", "test"); + } + + @Test + @Ignore + public void inputTest() { + Console.log("Please input something: "); + String input = Console.input(); + Console.log(input); + } + + @Test + @Ignore + public void printProgressTest() { + for(int i = 0; i < 100; i++) { + Console.printProgress('#', 100, i / 100D); + ThreadUtil.sleep(200); + } + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/lang/DictTest.java b/hutool-core/src/test/java/cn/hutool/core/lang/DictTest.java new file mode 100644 index 000000000..655e74491 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/lang/DictTest.java @@ -0,0 +1,20 @@ +package cn.hutool.core.lang; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.lang.Dict; + +public class DictTest { + @Test + public void dictTest(){ + Dict dict = Dict.create() + .set("key1", 1)//int + .set("key2", 1000L)//long + .set("key3", DateTime.now());//Date + + Long v2 = dict.getLong("key2"); + Assert.assertEquals(Long.valueOf(1000L), v2); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/lang/ObjectIdTest.java b/hutool-core/src/test/java/cn/hutool/core/lang/ObjectIdTest.java new file mode 100644 index 000000000..fe0b57404 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/lang/ObjectIdTest.java @@ -0,0 +1,33 @@ +package cn.hutool.core.lang; + +import java.util.HashSet; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +/** + * ObjectId单元测试 + * + * @author looly + * + */ +public class ObjectIdTest { + + @Test + public void distinctTest() { + //生成10000个id测试是否重复 + HashSet set = new HashSet<>(); + for(int i = 0; i < 10000; i++) { + set.add(ObjectId.next()); + } + + Assert.assertEquals(10000, set.size()); + } + + @Test + @Ignore + public void nextTest() { + Console.log(ObjectId.next()); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/lang/RangeTest.java b/hutool-core/src/test/java/cn/hutool/core/lang/RangeTest.java new file mode 100644 index 000000000..b9e12e5ca --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/lang/RangeTest.java @@ -0,0 +1,57 @@ +package cn.hutool.core.lang; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.date.DateField; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.lang.Range; + +/** + * {@link Range} 单元测试 + * @author Looly + * + */ +public class RangeTest { + + @Test + public void dateRangeTest() { + DateTime start = DateUtil.parse("2017-01-01"); + DateTime end = DateUtil.parse("2017-01-02"); + + final Range range = new Range(start, end, new Range.Steper(){ + + @Override + public DateTime step(DateTime current, DateTime end, int index) { + if(current.isAfterOrEquals(end)) { + return null; + } + return current.offsetNew(DateField.DAY_OF_YEAR, 1); + } + + }); + + Assert.assertTrue(range.hasNext()); + Assert.assertEquals(range.next(), DateUtil.parse("2017-01-01")); + Assert.assertTrue(range.hasNext()); + Assert.assertEquals(range.next(), DateUtil.parse("2017-01-02")); + Assert.assertFalse(range.hasNext()); + } + + @Test + public void intRangeTest() { + final Range range = new Range(1, 1, new Range.Steper(){ + + @Override + public Integer step(Integer current, Integer end, int index) { + return current >= end ? null : current +10; + } + + }); + + Assert.assertTrue(range.hasNext()); + Assert.assertEquals(Integer.valueOf(1), range.next()); + Assert.assertFalse(range.hasNext()); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/lang/SimhashTest.java b/hutool-core/src/test/java/cn/hutool/core/lang/SimhashTest.java new file mode 100644 index 000000000..1a9dccc82 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/lang/SimhashTest.java @@ -0,0 +1,24 @@ +package cn.hutool.core.lang; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.text.Simhash; +import cn.hutool.core.util.StrUtil; + +public class SimhashTest { + + @Test + public void simTest() { + String text1 = "我是 一个 普通 字符串"; + String text2 = "我是 一个 普通 字符串"; + + Simhash simhash = new Simhash(); + long hash = simhash.hash(StrUtil.split(text1, ' ')); + Assert.assertTrue(hash != 0); + + simhash.store(hash); + boolean duplicate = simhash.equals(StrUtil.split(text2, ' ')); + Assert.assertTrue(duplicate); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/lang/SnowflakeTest.java b/hutool-core/src/test/java/cn/hutool/core/lang/SnowflakeTest.java new file mode 100644 index 000000000..c46b72302 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/lang/SnowflakeTest.java @@ -0,0 +1,46 @@ +package cn.hutool.core.lang; + +import java.util.HashSet; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Snowflake单元测试 + * @author Looly + * + */ +public class SnowflakeTest { + + @Test + public void snowflakeTest1(){ + //构建Snowflake,提供终端ID和数据中心ID + Snowflake idWorker = new Snowflake(0, 0); + long nextId = idWorker.nextId(); + Assert.assertTrue(nextId > 0); + } + + @Test + public void snowflakeTest(){ + HashSet hashSet = new HashSet<>(); + + //构建Snowflake,提供终端ID和数据中心ID + Snowflake idWorker = new Snowflake(0, 0); + for (int i = 0; i < 1000; i++) { + long id = idWorker.nextId(); + hashSet.add(id); + } + Assert.assertEquals(1000L, hashSet.size()); + } + + @Test + public void snowflakeGetTest(){ + //构建Snowflake,提供终端ID和数据中心ID + Snowflake idWorker = new Snowflake(1, 2); + long nextId = idWorker.nextId(); + + Assert.assertEquals(1, idWorker.getWorkerId(nextId)); + Assert.assertEquals(2, idWorker.getDataCenterId(nextId)); + Assert.assertTrue(idWorker.getGenerateDateTime(nextId) - System.currentTimeMillis() < 10); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/lang/StrFormatterTest.java b/hutool-core/src/test/java/cn/hutool/core/lang/StrFormatterTest.java new file mode 100644 index 000000000..882ce399a --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/lang/StrFormatterTest.java @@ -0,0 +1,24 @@ +package cn.hutool.core.lang; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.text.StrFormatter; + +public class StrFormatterTest { + + @Test + public void formatTest(){ + //通常使用 + String result1 = StrFormatter.format("this is {} for {}", "a", "b"); + Assert.assertEquals("this is a for b", result1); + + //转义{} + String result2 = StrFormatter.format("this is \\{} for {}", "a", "b"); + Assert.assertEquals("this is {} for a", result2); + + //转义\ + String result3 = StrFormatter.format("this is \\\\{} for {}", "a", "b"); + Assert.assertEquals("this is \\a for b", result3); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/lang/StrSpliterTest.java b/hutool-core/src/test/java/cn/hutool/core/lang/StrSpliterTest.java new file mode 100644 index 000000000..c49e66534 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/lang/StrSpliterTest.java @@ -0,0 +1,48 @@ +package cn.hutool.core.lang; + +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.text.StrSpliter; + +/** + * {@link StrSpliter} 单元测试 + * @author Looly + * + */ +public class StrSpliterTest { + + @Test + public void splitByCharTest(){ + String str1 = "a, ,efedsfs, ddf"; + List split = StrSpliter.split(str1, ',', 0, true, true); + Assert.assertEquals("ddf", split.get(2)); + Assert.assertEquals(3, split.size()); + } + + @Test + public void splitByStrTest(){ + String str1 = "aabbccaaddaaee"; + List split = StrSpliter.split(str1, "aa", 0, true, true); + Assert.assertEquals("ee", split.get(2)); + Assert.assertEquals(3, split.size()); + } + + @Test + public void splitByBlankTest(){ + String str1 = "aa bbccaa ddaaee"; + List split = StrSpliter.split(str1, 0); + Assert.assertEquals("ddaaee", split.get(2)); + Assert.assertEquals(3, split.size()); + } + + @Test + public void splitPathTest(){ + String str1 = "/use/local/bin"; + List split = StrSpliter.splitPath(str1, 0); + Assert.assertEquals("bin", split.get(2)); + Assert.assertEquals(3, split.size()); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/lang/TextSimilarityTest.java b/hutool-core/src/test/java/cn/hutool/core/lang/TextSimilarityTest.java new file mode 100644 index 000000000..08bd83ca9 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/lang/TextSimilarityTest.java @@ -0,0 +1,26 @@ +package cn.hutool.core.lang; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.text.TextSimilarity; + +/** + * 文本相似度计算工具类单元测试 + * @author looly + * + */ +public class TextSimilarityTest { + + @Test + public void similarDegreeTest() { + String a = "我是一个文本,独一无二的文本"; + String b = "一个文本,独一无二的文本"; + + double degree = TextSimilarity.similar(a, b); + Assert.assertEquals(0.8571428571428571D, degree, 16); + + String similarPercent = TextSimilarity.similar(a, b, 2); + Assert.assertEquals("85.71%", similarPercent); + } +} \ No newline at end of file diff --git a/hutool-core/src/test/java/cn/hutool/core/lang/ValidatorTest.java b/hutool-core/src/test/java/cn/hutool/core/lang/ValidatorTest.java new file mode 100644 index 000000000..53599f60e --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/lang/ValidatorTest.java @@ -0,0 +1,119 @@ +package cn.hutool.core.lang; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.exceptions.ValidateException; +import cn.hutool.core.lang.Validator; + +/** + * 验证器单元测试 + * @author Looly + * + */ +public class ValidatorTest { + + @Test + public void isNumberTest() { + Assert.assertTrue(Validator.isNumber("45345365465")); + Assert.assertTrue(Validator.isNumber("0004545435")); + Assert.assertTrue(Validator.isNumber("5.222")); + Assert.assertTrue(Validator.isNumber("0.33323")); + } + + @Test + public void isLetterTest() { + Assert.assertTrue(Validator.isLetter("asfdsdsfds")); + Assert.assertTrue(Validator.isLetter("asfdsdfdsfVCDFDFGdsfds")); + Assert.assertTrue(Validator.isLetter("asfdsdf你好dsfVCDFDFGdsfds")); + } + + @Test + public void isUperCaseTest() { + Assert.assertTrue(Validator.isUpperCase("VCDFDFG")); + Assert.assertTrue(Validator.isUpperCase("ASSFD")); + + Assert.assertFalse(Validator.isUpperCase("asfdsdsfds")); + Assert.assertFalse(Validator.isUpperCase("ASSFD你好")); + } + + @Test + public void isLowerCaseTest() { + Assert.assertTrue(Validator.isLowerCase("asfdsdsfds")); + + Assert.assertFalse(Validator.isLowerCase("aaaa你好")); + Assert.assertFalse(Validator.isLowerCase("VCDFDFG")); + Assert.assertFalse(Validator.isLowerCase("ASSFD")); + Assert.assertFalse(Validator.isLowerCase("ASSFD你好")); + } + + @Test + public void isBirthdayTest(){ + boolean b = Validator.isBirthday("20150101"); + Assert.assertTrue(b); + boolean b2 = Validator.isBirthday("2015-01-01"); + Assert.assertTrue(b2); + boolean b3 = Validator.isBirthday("2015.01.01"); + Assert.assertTrue(b3); + boolean b4 = Validator.isBirthday("2015年01月01日"); + Assert.assertTrue(b4); + boolean b5 = Validator.isBirthday("2015.01.01"); + Assert.assertTrue(b5); + boolean b6 = Validator.isBirthday("2018-08-15"); + Assert.assertTrue(b6); + + //验证年非法 + Assert.assertFalse(Validator.isBirthday("2095.05.01")); + //验证月非法 + Assert.assertFalse(Validator.isBirthday("2015.13.01")); + //验证日非法 + Assert.assertFalse(Validator.isBirthday("2015.02.29")); + } + + @Test + public void isCitizenIdTest(){ + boolean b = Validator.isCitizenId("150218199012123389"); + Assert.assertTrue(b); + } + + @Test(expected=ValidateException.class) + public void validateTest() throws ValidateException{ + Validator.validateChinese("我是一段zhongwen", "内容中包含非中文"); + } + + @Test + public void isEmailTest() { + boolean email = Validator.isEmail("abc_cde@163.com"); + Assert.assertTrue(email); + boolean email1 = Validator.isEmail("abc_%cde@163.com"); + Assert.assertTrue(email1); + boolean email2 = Validator.isEmail("abc_%cde@aaa.c"); + Assert.assertTrue(email2); + boolean email3 = Validator.isEmail("xiaolei.lu@aaa.b"); + Assert.assertTrue(email3); + boolean email4 = Validator.isEmail("xiaolei.Lu@aaa.b"); + Assert.assertTrue(email4); + } + + @Test + public void isMobileTest() { + boolean m1 = Validator.isMobile("13900221432"); + Assert.assertTrue(m1); + boolean m2 = Validator.isMobile("015100221432"); + Assert.assertTrue(m2); + boolean m3 = Validator.isMobile("+8618600221432"); + Assert.assertTrue(m3); + } + + @Test + public void isMatchTest() { + String url = "http://aaa-bbb.somthing.com/a.php?a=b&c=2"; + Assert.assertTrue(Validator.isMactchRegex(PatternPool.URL_HTTP, url)); + + url = "https://aaa-bbb.somthing.com/a.php?a=b&c=2"; + Assert.assertTrue(Validator.isMactchRegex(PatternPool.URL_HTTP, url)); + + url = "https://aaa-bbb.somthing.com:8080/a.php?a=b&c=2"; + Assert.assertTrue(Validator.isMactchRegex(PatternPool.URL_HTTP, url)); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/lang/WeightRandomTest.java b/hutool-core/src/test/java/cn/hutool/core/lang/WeightRandomTest.java new file mode 100644 index 000000000..8ea09fddd --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/lang/WeightRandomTest.java @@ -0,0 +1,20 @@ +package cn.hutool.core.lang; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.collection.CollUtil; + +public class WeightRandomTest { + + @Test + public void weightRandomTest() { + WeightRandom random = WeightRandom.create(); + random.add("A", 10); + random.add("B", 50); + random.add("C", 100); + + String result = random.next(); + Assert.assertTrue(CollUtil.newArrayList("A", "B", "C").contains(result)); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/lang/test/bean/ExamInfoDict.java b/hutool-core/src/test/java/cn/hutool/core/lang/test/bean/ExamInfoDict.java new file mode 100644 index 000000000..56fc09cd5 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/lang/test/bean/ExamInfoDict.java @@ -0,0 +1,66 @@ +package cn.hutool.core.lang.test.bean; + +import java.io.Serializable; +import java.util.Objects; + +/** + * + * @author 质量过关 + * + */ +public class ExamInfoDict implements Serializable { + private static final long serialVersionUID = 3640936499125004525L; + + // 主键 + private Integer id; // 可当作题号 + // 试题类型 客观题 0主观题 1 + private Integer examType; + // 试题是否作答 + private Integer answerIs; + + public Integer getId() { + return id; + } + public Integer getId(Integer defaultValue) { + return this.id == null ? defaultValue : this.id; + } + public void setId(Integer id) { + this.id = id; + } + + public Integer getExamType() { + return examType; + } + public void setExamType(Integer examType) { + this.examType = examType; + } + + public Integer getAnswerIs() { + return answerIs; + } + public void setAnswerIs(Integer answerIs) { + this.answerIs = answerIs; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ExamInfoDict that = (ExamInfoDict) o; + return Objects.equals(id, that.id) && Objects.equals(examType, that.examType) && Objects.equals(answerIs, that.answerIs); + } + + @Override + public int hashCode() { + return Objects.hash(id, examType, answerIs); + } + + @Override + public String toString() { + return "ExamInfoDict{" + "id=" + id + ", examType=" + examType + ", answerIs=" + answerIs + '}'; + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/lang/test/bean/UserInfoDict.java b/hutool-core/src/test/java/cn/hutool/core/lang/test/bean/UserInfoDict.java new file mode 100644 index 000000000..c0ab8a893 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/lang/test/bean/UserInfoDict.java @@ -0,0 +1,79 @@ +package cn.hutool.core.lang.test.bean; + +import java.io.Serializable; +import java.util.List; +import java.util.Objects; + +/** + * 用户信息 + * @author 质量过关 + * + */ +public class UserInfoDict implements Serializable { + private static final long serialVersionUID = -936213991463284306L; + // 用户Id + private Integer id; + // 要展示的名字 + private String realName; + // 头像地址 + private String photoPath; + private List examInfoDict; + private UserInfoRedundCount userInfoRedundCount; + + public Integer getId() { + return id; + } + public void setId(Integer id) { + this.id = id; + } + + public String getRealName() { + return realName; + } + public void setRealName(String realName) { + this.realName = realName; + } + + public String getPhotoPath() { + return photoPath; + } + public void setPhotoPath(String photoPath) { + this.photoPath = photoPath; + } + + public List getExamInfoDict() { + return examInfoDict; + } + public void setExamInfoDict(List examInfoDict) { + this.examInfoDict = examInfoDict; + } + + public UserInfoRedundCount getUserInfoRedundCount() { + return userInfoRedundCount; + } + public void setUserInfoRedundCount(UserInfoRedundCount userInfoRedundCount) { + this.userInfoRedundCount = userInfoRedundCount; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + UserInfoDict that = (UserInfoDict) o; + return Objects.equals(id, that.id) && Objects.equals(realName, that.realName) && Objects.equals(photoPath, that.photoPath) && Objects.equals(examInfoDict, that.examInfoDict); + } + + @Override + public int hashCode() { + return Objects.hash(id, realName, photoPath, examInfoDict); + } + + @Override + public String toString() { + return "UserInfoDict [id=" + id + ", realName=" + realName + ", photoPath=" + photoPath + ", examInfoDict=" + examInfoDict + ", userInfoRedundCount=" + userInfoRedundCount + "]"; + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/lang/test/bean/UserInfoRedundCount.java b/hutool-core/src/test/java/cn/hutool/core/lang/test/bean/UserInfoRedundCount.java new file mode 100644 index 000000000..a5f4e5b13 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/lang/test/bean/UserInfoRedundCount.java @@ -0,0 +1,38 @@ +package cn.hutool.core.lang.test.bean; + +import java.io.Serializable; + +public class UserInfoRedundCount implements Serializable { + + private static final long serialVersionUID = -8397291070139255181L; + private String finishedRatio; // 完成率 + + private Integer ownershipExamCount; // 自己有多少道题 + + private Integer answeredExamCount; // 当前回答了多少道题 + + public Integer getOwnershipExamCount() { + return ownershipExamCount; + } + + public void setOwnershipExamCount(Integer ownershipExamCount) { + this.ownershipExamCount = ownershipExamCount; + } + + public Integer getAnsweredExamCount() { + return answeredExamCount; + } + + public void setAnsweredExamCount(Integer answeredExamCount) { + this.answeredExamCount = answeredExamCount; + } + + public String getFinishedRatio() { + return finishedRatio; + } + + public void setFinishedRatio(String finishedRatio) { + this.finishedRatio = finishedRatio; + } + +} diff --git a/hutool-core/src/test/java/cn/hutool/core/map/CamelCaseMapTest.java b/hutool-core/src/test/java/cn/hutool/core/map/CamelCaseMapTest.java new file mode 100644 index 000000000..03c81a649 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/map/CamelCaseMapTest.java @@ -0,0 +1,23 @@ +package cn.hutool.core.map; + +import org.junit.Assert; +import org.junit.Test; + +public class CamelCaseMapTest { + + @Test + public void caseInsensitiveMapTest() { + CamelCaseMap map = new CamelCaseMap<>(); + map.put("customKey", "OK"); + Assert.assertEquals("OK", map.get("customKey")); + Assert.assertEquals("OK", map.get("custom_key")); + } + + @Test + public void caseInsensitiveLinkedMapTest() { + CamelCaseLinkedMap map = new CamelCaseLinkedMap<>(); + map.put("customKey", "OK"); + Assert.assertEquals("OK", map.get("customKey")); + Assert.assertEquals("OK", map.get("custom_key")); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/map/CaseInsensitiveMapTest.java b/hutool-core/src/test/java/cn/hutool/core/map/CaseInsensitiveMapTest.java new file mode 100644 index 000000000..1dbdec34a --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/map/CaseInsensitiveMapTest.java @@ -0,0 +1,23 @@ +package cn.hutool.core.map; + +import org.junit.Assert; +import org.junit.Test; + +public class CaseInsensitiveMapTest { + + @Test + public void caseInsensitiveMapTest() { + CaseInsensitiveMap map = new CaseInsensitiveMap<>(); + map.put("aAA", "OK"); + Assert.assertEquals("OK", map.get("aaa")); + Assert.assertEquals("OK", map.get("AAA")); + } + + @Test + public void caseInsensitiveLinkedMapTest() { + CaseInsensitiveLinkedMap map = new CaseInsensitiveLinkedMap<>(); + map.put("aAA", "OK"); + Assert.assertEquals("OK", map.get("aaa")); + Assert.assertEquals("OK", map.get("AAA")); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/map/MapUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/map/MapUtilTest.java new file mode 100644 index 000000000..1356c1ae9 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/map/MapUtilTest.java @@ -0,0 +1,100 @@ +package cn.hutool.core.map; + +import java.util.Map; +import java.util.Map.Entry; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Editor; +import cn.hutool.core.lang.Filter; + +public class MapUtilTest { + + @Test + public void filterTest() { + Map map = MapUtil.newHashMap(); + map.put("a", "1"); + map.put("b", "2"); + map.put("c", "3"); + map.put("d", "4"); + + Map map2 = MapUtil.filter(map, new Filter>() { + + @Override + public boolean accept(Entry t) { + if (Convert.toInt(t.getValue()) % 2 == 0) { + return true; + } + return false; + } + }); + + Assert.assertEquals(2, map2.size()); + + Assert.assertEquals("2", map2.get("b")); + Assert.assertEquals("4", map2.get("d")); + } + + @Test + public void filterForEditorTest() { + Map map = MapUtil.newHashMap(); + map.put("a", "1"); + map.put("b", "2"); + map.put("c", "3"); + map.put("d", "4"); + + Map map2 = MapUtil.filter(map, new Editor>() { + + @Override + public Entry edit(Entry t) { + // 修改每个值使之*10 + t.setValue(t.getValue() + "0"); + return t; + } + }); + + Assert.assertEquals(4, map2.size()); + + Assert.assertEquals("10", map2.get("a")); + Assert.assertEquals("20", map2.get("b")); + Assert.assertEquals("30", map2.get("c")); + Assert.assertEquals("40", map2.get("d")); + } + + @Test + public void reverseTest() { + Map map = MapUtil.newHashMap(); + map.put("a", "1"); + map.put("b", "2"); + map.put("c", "3"); + map.put("d", "4"); + + Map map2 = MapUtil.reverse(map); + + Assert.assertEquals("a", map2.get("1")); + Assert.assertEquals("b", map2.get("2")); + Assert.assertEquals("c", map2.get("3")); + Assert.assertEquals("d", map2.get("4")); + } + + @Test + public void toObjectArrayTest() { + Map map = MapUtil.newHashMap(true); + map.put("a", "1"); + map.put("b", "2"); + map.put("c", "3"); + map.put("d", "4"); + + Object[][] objectArray = MapUtil.toObjectArray(map); + Assert.assertEquals("a", objectArray[0][0]); + Assert.assertEquals("1", objectArray[0][1]); + Assert.assertEquals("b", objectArray[1][0]); + Assert.assertEquals("2", objectArray[1][1]); + Assert.assertEquals("c", objectArray[2][0]); + Assert.assertEquals("3", objectArray[2][1]); + Assert.assertEquals("d", objectArray[3][0]); + Assert.assertEquals("4", objectArray[3][1]); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/math/ArrangementTest.java b/hutool-core/src/test/java/cn/hutool/core/math/ArrangementTest.java new file mode 100644 index 000000000..622490c00 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/math/ArrangementTest.java @@ -0,0 +1,54 @@ +package cn.hutool.core.math; + +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +/** + * 排列单元测试 + * @author looly + * + */ +public class ArrangementTest { + + @Test + public void arrangementTest() { + long result = Arrangement.count(4, 2); + Assert.assertEquals(12, result); + + result = Arrangement.count(4, 1); + Assert.assertEquals(4, result); + + result = Arrangement.count(4, 0); + Assert.assertEquals(1, result); + + long resultAll = Arrangement.countAll(4); + Assert.assertEquals(64, resultAll); + } + + @Test + public void selectTest() { + Arrangement arrangement = new Arrangement(new String[] { "1", "2", "3", "4" }); + List list = arrangement.select(2); + Assert.assertEquals(Arrangement.count(4, 2), list.size()); + Assert.assertArrayEquals(new String[] {"1", "2"}, list.get(0)); + Assert.assertArrayEquals(new String[] {"1", "3"}, list.get(1)); + Assert.assertArrayEquals(new String[] {"1", "4"}, list.get(2)); + Assert.assertArrayEquals(new String[] {"2", "1"}, list.get(3)); + Assert.assertArrayEquals(new String[] {"2", "3"}, list.get(4)); + Assert.assertArrayEquals(new String[] {"2", "4"}, list.get(5)); + Assert.assertArrayEquals(new String[] {"3", "1"}, list.get(6)); + Assert.assertArrayEquals(new String[] {"3", "2"}, list.get(7)); + Assert.assertArrayEquals(new String[] {"3", "4"}, list.get(8)); + Assert.assertArrayEquals(new String[] {"4", "1"}, list.get(9)); + Assert.assertArrayEquals(new String[] {"4", "2"}, list.get(10)); + Assert.assertArrayEquals(new String[] {"4", "3"}, list.get(11)); + + List selectAll = arrangement.selectAll(); + Assert.assertEquals(Arrangement.countAll(4), selectAll.size()); + + List list2 = arrangement.select(0); + Assert.assertTrue(1 == list2.size()); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/math/CombinationTest.java b/hutool-core/src/test/java/cn/hutool/core/math/CombinationTest.java new file mode 100644 index 000000000..cd24b568f --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/math/CombinationTest.java @@ -0,0 +1,54 @@ +package cn.hutool.core.math; + +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +/** + * 组合单元测试 + * + * @author looly + * + */ +public class CombinationTest { + + @Test + public void countTest() { + long result = Combination.count(5, 2); + Assert.assertEquals(10, result); + + result = Combination.count(5, 5); + Assert.assertEquals(1, result); + + result = Combination.count(5, 0); + Assert.assertEquals(1, result); + + long resultAll = Combination.countAll(5); + Assert.assertEquals(31, resultAll); + } + + @Test + public void selectTest() { + Combination combination = new Combination(new String[] { "1", "2", "3", "4", "5" }); + List list = combination.select(2); + Assert.assertEquals(Combination.count(5, 2), list.size()); + + Assert.assertArrayEquals(new String[] {"1", "2"}, list.get(0)); + Assert.assertArrayEquals(new String[] {"1", "3"}, list.get(1)); + Assert.assertArrayEquals(new String[] {"1", "4"}, list.get(2)); + Assert.assertArrayEquals(new String[] {"1", "5"}, list.get(3)); + Assert.assertArrayEquals(new String[] {"2", "3"}, list.get(4)); + Assert.assertArrayEquals(new String[] {"2", "4"}, list.get(5)); + Assert.assertArrayEquals(new String[] {"2", "5"}, list.get(6)); + Assert.assertArrayEquals(new String[] {"3", "4"}, list.get(7)); + Assert.assertArrayEquals(new String[] {"3", "5"}, list.get(8)); + Assert.assertArrayEquals(new String[] {"4", "5"}, list.get(9)); + + List selectAll = combination.selectAll(); + Assert.assertEquals(Combination.countAll(5), selectAll.size()); + + List list2 = combination.select(0); + Assert.assertTrue(1 == list2.size()); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/net/NetUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/net/NetUtilTest.java new file mode 100644 index 000000000..6a9910b4d --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/net/NetUtilTest.java @@ -0,0 +1,57 @@ +package cn.hutool.core.net; + +import java.net.InetAddress; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.lang.PatternPool; +import cn.hutool.core.net.NetUtil; +import cn.hutool.core.util.ReUtil; + +/** + * NetUtil单元测试 + * + * @author Looly + * + */ +public class NetUtilTest { + + @Test + @Ignore + public void getLocalhostStrTest() { + String localhost = NetUtil.getLocalhostStr(); + Assert.assertNotNull(localhost); + } + + @Test + @Ignore + public void getLocalhostTest() { + InetAddress localhost = NetUtil.getLocalhost(); + Assert.assertNotNull(localhost); + } + + @Test + @Ignore + public void getLocalMacAddressTest() { + String macAddress = NetUtil.getLocalMacAddress(); + Assert.assertNotNull(macAddress); + + // 验证MAC地址正确 + boolean match = ReUtil.isMatch(PatternPool.MAC_ADDRESS, macAddress); + Assert.assertTrue(match); + } + + @Test + public void longToIpTest() { + String ipv4 = NetUtil.longToIpv4(2130706433L); + Assert.assertEquals("127.0.0.1", ipv4); + } + + @Test + public void ipToLongTest() { + long ipLong = NetUtil.ipv4ToLong("127.0.0.1"); + Assert.assertEquals(2130706433L, ipLong); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/swing/ClipboardMonitorTest.java b/hutool-core/src/test/java/cn/hutool/core/swing/ClipboardMonitorTest.java new file mode 100644 index 000000000..200534790 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/swing/ClipboardMonitorTest.java @@ -0,0 +1,43 @@ +package cn.hutool.core.swing; + +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.Transferable; + +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.lang.Console; +import cn.hutool.core.swing.clipboard.ClipboardListener; +import cn.hutool.core.swing.clipboard.ClipboardUtil; + +public class ClipboardMonitorTest { + + @Test + @Ignore + public void monitorTest() { + // 第一个监听 + ClipboardUtil.listen(new ClipboardListener() { + + @Override + public Transferable onChange(Clipboard clipboard, Transferable contents) { + Object object = ClipboardUtil.getStr(contents); + Console.log("1# {}", object); + return contents; + } + + }, false); + + // 第二个监听 + ClipboardUtil.listen(new ClipboardListener() { + + @Override + public Transferable onChange(Clipboard clipboard, Transferable contents) { + Object object = ClipboardUtil.getStr(contents); + Console.log("2# {}", object); + return contents; + } + + }); + + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/swing/ClipboardUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/swing/ClipboardUtilTest.java new file mode 100644 index 000000000..c4ac33979 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/swing/ClipboardUtilTest.java @@ -0,0 +1,28 @@ +package cn.hutool.core.swing; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.swing.clipboard.ClipboardUtil; + +/** + * 剪贴板工具类单元测试 + * + * @author looly + * + */ +public class ClipboardUtilTest { + + @Test + public void setAndGetStrTest() { + try { + ClipboardUtil.setStr("test"); + + String test = ClipboardUtil.getStr(); + Assert.assertEquals("test", test); + } catch (java.awt.HeadlessException e) { + // 忽略 No X11 DISPLAY variable was set, but this program performed an operation which requires it. + // ignore + } + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/swing/DesktopUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/swing/DesktopUtilTest.java new file mode 100644 index 000000000..ad2f1bd91 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/swing/DesktopUtilTest.java @@ -0,0 +1,13 @@ +package cn.hutool.core.swing; + +import org.junit.Ignore; +import org.junit.Test; + +public class DesktopUtilTest { + + @Test + @Ignore + public void browseTest() { + DesktopUtil.browse("https://www.hutool.club"); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/swing/RobotUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/swing/RobotUtilTest.java new file mode 100644 index 000000000..5d753de4c --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/swing/RobotUtilTest.java @@ -0,0 +1,15 @@ +package cn.hutool.core.swing; + +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.io.FileUtil; + +public class RobotUtilTest { + + @Test + @Ignore + public void captureScreenTest() { + RobotUtil.captureScreen(FileUtil.file("e:/screen.jpg")); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/text/StrBuilderTest.java b/hutool-core/src/test/java/cn/hutool/core/text/StrBuilderTest.java new file mode 100644 index 000000000..a81262abf --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/text/StrBuilderTest.java @@ -0,0 +1,89 @@ +package cn.hutool.core.text; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.date.TimeInterval; +import cn.hutool.core.lang.Console; + +/** + * StrBuilder单元测试 + * @author looly + * + */ +public class StrBuilderTest { + + /** + * StrBuilder的性能测试 + */ + @Test + @Ignore + public void benchTest() { + TimeInterval timer = DateUtil.timer(); + StrBuilder builder = StrBuilder.create(); + for(int i =0; i< 1000000; i++) { + builder.append("test"); + builder.reset(); + } + Console.log(timer.interval()); + + timer.restart(); + StringBuilder b2 = new StringBuilder(); + for(int i =0; i< 1000000; i++) { + b2.append("test"); + b2 = new StringBuilder(); + } + Console.log(timer.interval()); + } + + @Test + public void appendTest() { + StrBuilder builder = StrBuilder.create(); + builder.append("aaa").append("你好").append('r'); + Assert.assertEquals("aaa你好r", builder.toString()); + } + + @Test + public void insertTest() { + StrBuilder builder = StrBuilder.create(1); + builder.append("aaa").append("你好").append('r'); + builder.insert(3, "数据插入"); + Assert.assertEquals("aaa数据插入你好r", builder.toString()); + } + + @Test + public void insertTest2() { + StrBuilder builder = StrBuilder.create(1); + builder.append("aaa").append("你好").append('r'); + builder.insert(8, "数据插入"); + Assert.assertEquals("aaa你好r 数据插入", builder.toString()); + } + + @Test + public void resetTest() { + StrBuilder builder = StrBuilder.create(1); + builder.append("aaa").append("你好").append('r'); + builder.insert(3, "数据插入"); + builder.reset(); + Assert.assertEquals("", builder.toString()); + } + + @Test + public void resetTest2() { + StrBuilder builder = StrBuilder.create(1); + builder.append("aaa").append("你好").append('r'); + builder.insert(3, "数据插入"); + builder.reset(); + builder.append("bbb".toCharArray()); + Assert.assertEquals("bbb", builder.toString()); + } + + @Test + public void appendObjectTest() { + StrBuilder builder = StrBuilder.create(1); + builder.append(123).append(456.123D).append(true).append('\n'); + Assert.assertEquals("123456.123true\n", builder.toString()); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/text/UnicodeUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/text/UnicodeUtilTest.java new file mode 100644 index 000000000..44a61f327 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/text/UnicodeUtilTest.java @@ -0,0 +1,49 @@ +package cn.hutool.core.text; + +import org.junit.Assert; +import org.junit.Test; + +/** + * UnicodeUtil 单元测试 + * + * @author looly + * + */ +public class UnicodeUtilTest { + @Test + public void convertTest() { + String s = UnicodeUtil.toUnicode("aaa123中文", true); + Assert.assertEquals("aaa123\\u4e2d\\u6587", s); + + String s1 = UnicodeUtil.toString(s); + Assert.assertEquals("aaa123中文", s1); + } + + @Test + public void convertTest2() { + String str = "aaaa\\u0026bbbb\\u0026cccc"; + String unicode = UnicodeUtil.toString(str); + Assert.assertEquals("aaaa&bbbb&cccc", unicode); + } + + @Test + public void convertTest3() { + String str = "aaa\\u111"; + String res = UnicodeUtil.toString(str); + Assert.assertEquals("aaa\\u111", res); + } + + @Test + public void convertTest4() { + String str = "aaa\\U4e2d\\u6587\\u111\\urtyu\\u0026"; + String res = UnicodeUtil.toString(str); + Assert.assertEquals("aaa中文\\u111\\urtyu&", res); + } + + @Test + public void convertTest5() { + String str = "{\"code\":403,\"enmsg\":\"Product not found\",\"cnmsg\":\"\\u4ea7\\u54c1\\u4e0d\\u5b58\\u5728\\uff0c\\u6216\\u5df2\\u5220\\u9664\",\"data\":null}"; + String res = UnicodeUtil.toString(str); + Assert.assertEquals("{\"code\":403,\"enmsg\":\"Product not found\",\"cnmsg\":\"产品不存在,或已删除\",\"data\":null}", res); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/text/csv/CsvParserTest.java b/hutool-core/src/test/java/cn/hutool/core/text/csv/CsvParserTest.java new file mode 100644 index 000000000..bb93a6eb4 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/text/csv/CsvParserTest.java @@ -0,0 +1,48 @@ +package cn.hutool.core.text.csv; + +import java.io.StringReader; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; + +public class CsvParserTest { + + @Test + public void parseTest1() { + StringReader reader = StrUtil.getReader("aaa,b\"bba\",ccc"); + CsvParser parser = new CsvParser(reader, null); + CsvRow row = parser.nextRow(); + Assert.assertEquals("b\"bba\"", row.getRawList().get(1)); + IoUtil.close(parser); + } + + @Test + public void parseTest2() { + StringReader reader = StrUtil.getReader("aaa,\"bba\"bbb,ccc"); + CsvParser parser = new CsvParser(reader, null); + CsvRow row = parser.nextRow(); + Assert.assertEquals("\"bba\"bbb", row.getRawList().get(1)); + IoUtil.close(parser); + } + + @Test + public void parseTest3() { + StringReader reader = StrUtil.getReader("aaa,\"bba\",ccc"); + CsvParser parser = new CsvParser(reader, null); + CsvRow row = parser.nextRow(); + Assert.assertEquals("bba", row.getRawList().get(1)); + IoUtil.close(parser); + } + + @Test + public void parseTest4() { + StringReader reader = StrUtil.getReader("aaa,\"\",ccc"); + CsvParser parser = new CsvParser(reader, null); + CsvRow row = parser.nextRow(); + Assert.assertEquals("", row.getRawList().get(1)); + IoUtil.close(parser); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/text/csv/CsvReaderTest.java b/hutool-core/src/test/java/cn/hutool/core/text/csv/CsvReaderTest.java new file mode 100644 index 000000000..59d9e61fe --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/text/csv/CsvReaderTest.java @@ -0,0 +1,17 @@ +package cn.hutool.core.text.csv; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.util.CharsetUtil; + +public class CsvReaderTest { + + @Test + public void readTest() { + CsvReader reader = new CsvReader(); + CsvData data = reader.read(ResourceUtil.getReader("test.csv", CharsetUtil.CHARSET_UTF_8)); + Assert.assertEquals("关注\"对象\"", data.getRow(0).get(2)); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/text/csv/CsvUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/text/csv/CsvUtilTest.java new file mode 100644 index 000000000..6b5e8eaf8 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/text/csv/CsvUtilTest.java @@ -0,0 +1,36 @@ +package cn.hutool.core.text.csv; + +import java.util.List; + +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.CharsetUtil; + +public class CsvUtilTest { + + @Test + public void readTest() { + CsvReader reader = CsvUtil.getReader(); + //从文件中读取CSV数据 + CsvData data = reader.read(FileUtil.file("test.csv")); + List rows = data.getRows(); + for (CsvRow csvRow : rows) { + Assert.notEmpty(csvRow.getRawList()); + } + } + + @Test + @Ignore + public void writeTest() { + CsvWriter writer = CsvUtil.getWriter("e:/testWrite.csv", CharsetUtil.CHARSET_UTF_8); + writer.write( + new String[] {"a1", "b1", "c1", "123345346456745756756785656"}, + new String[] {"a2", "b2", "c2"}, + new String[] {"a3", "b3", "c3"} + ); + } + +} diff --git a/hutool-core/src/test/java/cn/hutool/core/thread/ConcurrencyTesterTest.java b/hutool-core/src/test/java/cn/hutool/core/thread/ConcurrencyTesterTest.java new file mode 100644 index 000000000..5999b97f6 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/thread/ConcurrencyTesterTest.java @@ -0,0 +1,25 @@ +package cn.hutool.core.thread; + +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.lang.Console; +import cn.hutool.core.util.RandomUtil; + +public class ConcurrencyTesterTest { + + @Test + @Ignore + public void concurrencyTesterTest() { + ConcurrencyTester tester = ThreadUtil.concurrencyTest(100, new Runnable() { + + @Override + public void run() { + long delay = RandomUtil.randomLong(100, 1000); + ThreadUtil.sleep(delay); + Console.log("{} test finished, delay: {}", Thread.currentThread().getName(), delay); + } + }); + Console.log(tester.getInterval()); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/thread/ThreadUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/thread/ThreadUtilTest.java new file mode 100644 index 000000000..ef240d436 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/thread/ThreadUtilTest.java @@ -0,0 +1,21 @@ +package cn.hutool.core.thread; + +import org.junit.Assert; +import org.junit.Test; + +public class ThreadUtilTest { + + @Test + public void executeTest() { + final boolean isValid = true; + + ThreadUtil.execute(new Runnable() { + + @Override + public void run() { + Assert.assertTrue(isValid); + } + }); + + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/util/ArrayUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/ArrayUtilTest.java new file mode 100644 index 000000000..eade5fb69 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/util/ArrayUtilTest.java @@ -0,0 +1,228 @@ +package cn.hutool.core.util; + +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.lang.Editor; +import cn.hutool.core.lang.Filter; + +/** + * {@link ArrayUtil} 数组工具单元测试 + * + * @author Looly + * + */ +public class ArrayUtilTest { + + @Test + public void isEmptyTest() { + int[] a = {}; + Assert.assertTrue(ArrayUtil.isEmpty(a)); + Assert.assertTrue(ArrayUtil.isEmpty((Object) a)); + int[] b = null; + Assert.assertTrue(ArrayUtil.isEmpty(b)); + Object c = null; + Assert.assertTrue(ArrayUtil.isEmpty(c)); + } + + @Test + public void isNotEmptyTest() { + int[] a = { 1, 2 }; + Assert.assertTrue(ArrayUtil.isNotEmpty(a)); + } + + @Test + public void newArrayTest() { + String[] newArray = ArrayUtil.newArray(String.class, 3); + Assert.assertEquals(3, newArray.length); + } + + @Test + public void cloneTest() { + Integer[] b = { 1, 2, 3 }; + Integer[] cloneB = ArrayUtil.clone(b); + Assert.assertArrayEquals(b, cloneB); + + int[] a = { 1, 2, 3 }; + int[] clone = ArrayUtil.clone(a); + Assert.assertArrayEquals(a, clone); + } + + @Test + public void filterTest() { + Integer[] a = { 1, 2, 3, 4, 5, 6 }; + Integer[] filter = ArrayUtil.filter(a, new Editor() { + @Override + public Integer edit(Integer t) { + return (t % 2 == 0) ? t : null; + } + }); + Assert.assertArrayEquals(filter, new Integer[] { 2, 4, 6 }); + } + + @Test + public void filterTestForFilter() { + Integer[] a = { 1, 2, 3, 4, 5, 6 }; + Integer[] filter = ArrayUtil.filter(a, new Filter() { + @Override + public boolean accept(Integer t) { + return t % 2 == 0; + } + }); + Assert.assertArrayEquals(filter, new Integer[] { 2, 4, 6 }); + } + + @Test + public void filterTestForEditor() { + Integer[] a = { 1, 2, 3, 4, 5, 6 }; + Integer[] filter = ArrayUtil.filter(a, new Editor() { + @Override + public Integer edit(Integer t) { + return (t % 2 == 0) ? t * 10 : t; + } + }); + Assert.assertArrayEquals(filter, new Integer[] { 1, 20, 3, 40, 5, 60 }); + } + + @Test + public void indexOfTest() { + Integer[] a = { 1, 2, 3, 4, 5, 6 }; + int index = ArrayUtil.indexOf(a, 3); + Assert.assertEquals(2, index); + + long[] b = { 1, 2, 3, 4, 5, 6 }; + int index2 = ArrayUtil.indexOf(b, 3); + Assert.assertEquals(2, index2); + } + + @Test + public void lastIndexOfTest() { + Integer[] a = { 1, 2, 3, 4, 3, 6 }; + int index = ArrayUtil.lastIndexOf(a, 3); + Assert.assertEquals(4, index); + + long[] b = { 1, 2, 3, 4, 3, 6 }; + int index2 = ArrayUtil.lastIndexOf(b, 3); + Assert.assertEquals(4, index2); + } + + @Test + public void containsTest() { + Integer[] a = { 1, 2, 3, 4, 3, 6 }; + boolean contains = ArrayUtil.contains(a, 3); + Assert.assertTrue(contains); + + long[] b = { 1, 2, 3, 4, 3, 6 }; + boolean contains2 = ArrayUtil.contains(b, 3); + Assert.assertTrue(contains2); + } + + @Test + public void mapTest() { + String[] keys = { "a", "b", "c" }; + Integer[] values = { 1, 2, 3 }; + Map map = ArrayUtil.zip(keys, values, true); + Assert.assertEquals(map.toString(), "{a=1, b=2, c=3}"); + } + + @Test + public void castTest() { + Object[] values = { "1", "2", "3" }; + String[] cast = (String[]) ArrayUtil.cast(String.class, values); + Assert.assertEquals(values[0], cast[0]); + Assert.assertEquals(values[1], cast[1]); + Assert.assertEquals(values[2], cast[2]); + } + + @Test + public void rangeTest() { + int[] range = ArrayUtil.range(0, 10); + Assert.assertEquals(0, range[0]); + Assert.assertEquals(1, range[1]); + Assert.assertEquals(2, range[2]); + Assert.assertEquals(3, range[3]); + Assert.assertEquals(4, range[4]); + Assert.assertEquals(5, range[5]); + Assert.assertEquals(6, range[6]); + Assert.assertEquals(7, range[7]); + Assert.assertEquals(8, range[8]); + Assert.assertEquals(9, range[9]); + } + + @Test + public void maxTest() { + int max = ArrayUtil.max(1, 2, 13, 4, 5); + Assert.assertEquals(13, max); + + long maxLong = ArrayUtil.max(1L, 2L, 13L, 4L, 5L); + Assert.assertEquals(13, maxLong); + + double maxDouble = ArrayUtil.max(1D, 2.4D, 13.0D, 4.55D, 5D); + Assert.assertEquals(13.0, maxDouble, 2); + } + + @Test + public void minTest() { + int min = ArrayUtil.min(1, 2, 13, 4, 5); + Assert.assertEquals(1, min); + + long minLong = ArrayUtil.min(1L, 2L, 13L, 4L, 5L); + Assert.assertEquals(1, minLong); + + double minDouble = ArrayUtil.min(1D, 2.4D, 13.0D, 4.55D, 5D); + Assert.assertEquals(1.0, minDouble, 2); + } + + @Test + public void appendTest() { + String[] a = { "1", "2", "3", "4" }; + String[] b = { "a", "b", "c" }; + + String[] result = ArrayUtil.append(a, b); + Assert.assertArrayEquals(new String[] { "1", "2", "3", "4", "a", "b", "c" }, result); + } + + @Test + public void insertTest() { + String[] a = { "1", "2", "3", "4" }; + String[] b = { "a", "b", "c" }; + + // 在-1位置插入,相当于在3位置插入 + String[] result = ArrayUtil.insert(a, -1, b); + Assert.assertArrayEquals(new String[] { "1", "2", "3", "a", "b", "c", "4" }, result); + + // 在第0个位置插入,既在数组前追加 + result = ArrayUtil.insert(a, 0, b); + Assert.assertArrayEquals(new String[] { "a", "b", "c", "1", "2", "3", "4" }, result); + + // 在第2个位置插入,既"3"之前 + result = ArrayUtil.insert(a, 2, b); + Assert.assertArrayEquals(new String[] { "1", "2", "a", "b", "c", "3", "4" }, result); + + // 在第4个位置插入,既"4"之后,相当于追加 + result = ArrayUtil.insert(a, 4, b); + Assert.assertArrayEquals(new String[] { "1", "2", "3", "4", "a", "b", "c" }, result); + + // 在第5个位置插入,由于数组长度为4,因此补null + result = ArrayUtil.insert(a, 5, b); + Assert.assertArrayEquals(new String[] { "1", "2", "3", "4", null, "a", "b", "c" }, result); + } + + @Test + public void joinTest() { + String[] array = {"aa", "bb", "cc", "dd"}; + String join = ArrayUtil.join(array, ",", "[", "]"); + Assert.assertEquals("[aa],[bb],[cc],[dd]", join); + } + + @Test + public void getArrayTypeTest() { + Class arrayType = ArrayUtil.getArrayType(int.class); + Assert.assertEquals(int[].class, arrayType); + + arrayType = ArrayUtil.getArrayType(String.class); + Assert.assertEquals(String[].class, arrayType); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/util/BooleanUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/BooleanUtilTest.java new file mode 100644 index 000000000..f7bea2dc1 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/util/BooleanUtilTest.java @@ -0,0 +1,24 @@ +package cn.hutool.core.util; + +import org.junit.Assert; +import org.junit.Test; + +public class BooleanUtilTest { + + @Test + public void toBooleanTest() { + Assert.assertTrue(BooleanUtil.toBoolean("true")); + Assert.assertTrue(BooleanUtil.toBoolean("yes")); + Assert.assertTrue(BooleanUtil.toBoolean("t")); + Assert.assertTrue(BooleanUtil.toBoolean("OK")); + Assert.assertTrue(BooleanUtil.toBoolean("1")); + Assert.assertTrue(BooleanUtil.toBoolean("On")); + Assert.assertTrue(BooleanUtil.toBoolean("是")); + Assert.assertTrue(BooleanUtil.toBoolean("对")); + Assert.assertTrue(BooleanUtil.toBoolean("真")); + + Assert.assertFalse(BooleanUtil.toBoolean("false")); + Assert.assertFalse(BooleanUtil.toBoolean("6455434")); + Assert.assertFalse(BooleanUtil.toBoolean("")); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/util/CharUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/CharUtilTest.java new file mode 100644 index 000000000..70b5c7fe0 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/util/CharUtilTest.java @@ -0,0 +1,23 @@ +package cn.hutool.core.util; + +import org.junit.Assert; +import org.junit.Test; + +public class CharUtilTest { + + @Test + public void trimTest() { + //此字符串中的第一个字符为不可见字符: '\u202a' + String str = "‪C:/Users/maple/Desktop/tone.txt"; + Assert.assertEquals('\u202a', str.charAt(0)); + Assert.assertTrue(CharUtil.isBlankChar(str.charAt(0))); + } + + @Test + public void isEmojiTest() { + String a = "莉🌹"; + Assert.assertFalse(CharUtil.isEmoji(a.charAt(0))); + Assert.assertTrue(CharUtil.isEmoji(a.charAt(1))); + + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/util/ClassLoaderUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/ClassLoaderUtilTest.java new file mode 100644 index 000000000..35aec5694 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/util/ClassLoaderUtilTest.java @@ -0,0 +1,16 @@ +package cn.hutool.core.util; + +import org.junit.Assert; +import org.junit.Test; + +public class ClassLoaderUtilTest { + + @Test + public void loadClassTest() { + String name = ClassLoaderUtil.loadClass("java.lang.Thread.State").getName(); + Assert.assertEquals("java.lang.Thread$State", name); + + name = ClassLoaderUtil.loadClass("java.lang.Thread$State").getName(); + Assert.assertEquals("java.lang.Thread$State", name); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/util/ClassUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/ClassUtilTest.java new file mode 100644 index 000000000..7cb494c7e --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/util/ClassUtilTest.java @@ -0,0 +1,105 @@ +package cn.hutool.core.util; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import org.junit.Assert; +import org.junit.Test; + +/** + * {@link ClassUtil} 单元测试 + * + * @author Looly + * + */ +public class ClassUtilTest { + + @Test + public void getClassNameTest() { + String className = ClassUtil.getClassName(ClassUtil.class, false); + Assert.assertEquals("cn.hutool.core.util.ClassUtil", className); + + String simpleClassName = ClassUtil.getClassName(ClassUtil.class, true); + Assert.assertEquals("ClassUtil", simpleClassName); + } + + @SuppressWarnings("unused") + class TestClass { + private String privateField; + protected String field; + + private void privateMethod() { + } + + public void publicMethod() { + } + } + + @SuppressWarnings("unused") + class TestSubClass extends TestClass { + private String subField; + + private void privateSubMethod() { + } + + public void publicSubMethod() { + } + + } + + @Test + public void getPublicMethod() { + Method superPublicMethod = ClassUtil.getPublicMethod(TestSubClass.class, "publicMethod"); + Assert.assertNotNull(superPublicMethod); + Method superPrivateMethod = ClassUtil.getPublicMethod(TestSubClass.class, "privateMethod"); + Assert.assertNull(superPrivateMethod); + + Method publicMethod = ClassUtil.getPublicMethod(TestSubClass.class, "publicSubMethod"); + Assert.assertNotNull(publicMethod); + Method privateMethod = ClassUtil.getPublicMethod(TestSubClass.class, "privateSubMethod"); + Assert.assertNull(privateMethod); + } + + @Test + public void getDeclaredMethod() throws Exception { + Method noMethod = ClassUtil.getDeclaredMethod(TestSubClass.class, "noMethod"); + Assert.assertNull(noMethod); + + Method privateMethod = ClassUtil.getDeclaredMethod(TestSubClass.class, "privateMethod"); + Assert.assertNotNull(privateMethod); + Method publicMethod = ClassUtil.getDeclaredMethod(TestSubClass.class, "publicMethod"); + Assert.assertNotNull(publicMethod); + + Method publicSubMethod = ClassUtil.getDeclaredMethod(TestSubClass.class, "publicSubMethod"); + Assert.assertNotNull(publicSubMethod); + Method privateSubMethod = ClassUtil.getDeclaredMethod(TestSubClass.class, "privateSubMethod"); + Assert.assertNotNull(privateSubMethod); + + } + + @Test + public void getDeclaredField() { + Field noField = ClassUtil.getDeclaredField(TestSubClass.class, "noField"); + Assert.assertNull(noField); + + // 获取不到父类字段 + Field field = ClassUtil.getDeclaredField(TestSubClass.class, "field"); + Assert.assertNull(field); + + Field subField = ClassUtil.getDeclaredField(TestSubClass.class, "subField"); + Assert.assertNotNull(subField); + } + + @Test + public void getClassPathTest() { + String classPath = ClassUtil.getClassPath(); + Assert.assertNotNull(classPath); + } + + @Test + public void getShortClassNameTest() { + String className = "cn.hutool.core.util.StrUtil"; + String result = ClassUtil.getShortClassName(className); + Assert.assertEquals("c.h.c.u.StrUtil", result); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/util/EnumUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/EnumUtilTest.java new file mode 100644 index 000000000..eafd9cbc0 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/util/EnumUtilTest.java @@ -0,0 +1,73 @@ +package cn.hutool.core.util; + +import java.util.List; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.collection.CollUtil; + +/** + * EnumUtil单元测试 + * + * @author looly + * + */ +public class EnumUtilTest { + + @Test + public void getNamesTest() { + List names = EnumUtil.getNames(TestEnum.class); + Assert.assertEquals(CollUtil.newArrayList("TEST1", "TEST2", "TEST3"), names); + } + + @Test + public void getFieldValuesTest() { + List types = EnumUtil.getFieldValues(TestEnum.class, "type"); + Assert.assertEquals(CollUtil.newArrayList("type1", "type2", "type3"), types); + } + + @Test + public void getFieldNamesTest() { + List names = EnumUtil.getFieldNames(TestEnum.class); + Assert.assertEquals(CollUtil.newArrayList("type", "name"), names); + } + + @Test + public void likeValueOfTest() { + TestEnum value = EnumUtil.likeValueOf(TestEnum.class, "type2"); + Assert.assertEquals(TestEnum.TEST2, value); + } + + @Test + public void getEnumMapTest() { + Map enumMap = EnumUtil.getEnumMap(TestEnum.class); + Assert.assertEquals(TestEnum.TEST1, enumMap.get("TEST1")); + } + + @Test + public void getNameFieldMapTest() { + Map enumMap = EnumUtil.getNameFieldMap(TestEnum.class, "type"); + Assert.assertEquals("type1", enumMap.get("TEST1")); + } + + public static enum TestEnum{ + TEST1("type1"), TEST2("type2"), TEST3("type3"); + + private TestEnum(String type) { + this.type = type; + } + + private String type; + private String name; + + public String getType() { + return this.type; + } + + public String getName() { + return this.name; + } + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/util/EscapeUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/EscapeUtilTest.java new file mode 100644 index 000000000..72fb5b381 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/util/EscapeUtilTest.java @@ -0,0 +1,17 @@ +package cn.hutool.core.util; + +import org.junit.Assert; +import org.junit.Test; + +public class EscapeUtilTest { + + @Test + public void escapeHtml4Test() { + String escapeHtml4 = EscapeUtil.escapeHtml4("你好"); + Assert.assertEquals("<a>你好</a>", escapeHtml4); + + String result = EscapeUtil.unescapeHtml4("振荡器类型"); + Assert.assertEquals("振荡器类型", result); + + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/util/HexUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/HexUtilTest.java new file mode 100644 index 000000000..9c620d63b --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/util/HexUtilTest.java @@ -0,0 +1,41 @@ +package cn.hutool.core.util; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.HexUtil; + +/** + * HexUtil单元测试 + * @author Looly + * + */ +public class HexUtilTest { + + @Test + public void hexStrTest(){ + String str = "我是一个字符串"; + + String hex = HexUtil.encodeHexStr(str, CharsetUtil.CHARSET_UTF_8); + String decodedStr = HexUtil.decodeHexStr(hex); + + Assert.assertEquals(str, decodedStr); + } + + @Test + public void toUnicodeHexTest() { + String unicodeHex = HexUtil.toUnicodeHex('\u2001'); + Assert.assertEquals("\\u2001", unicodeHex); + + unicodeHex = HexUtil.toUnicodeHex('你'); + Assert.assertEquals("\\u4f60", unicodeHex); + } + + @Test + public void isHexNumberTest() { + String a = "0x3544534F444"; + boolean isHex = HexUtil.isHexNumber(a); + Assert.assertTrue(isHex); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/util/IdUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/IdUtilTest.java new file mode 100644 index 000000000..f52adca64 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/util/IdUtilTest.java @@ -0,0 +1,143 @@ +package cn.hutool.core.util; + +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.collection.ConcurrentHashSet; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.date.TimeInterval; +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.lang.Console; +import cn.hutool.core.lang.Snowflake; +import cn.hutool.core.thread.ThreadUtil; + +/** + * {@link IdUtil} 单元测试 + * + * @author looly + * + */ +public class IdUtilTest { + + @Test + public void randomUUIDTest() { + String simpleUUID = IdUtil.simpleUUID(); + Assert.assertEquals(32, simpleUUID.length()); + + String randomUUID = IdUtil.randomUUID(); + Assert.assertEquals(36, randomUUID.length()); + } + + @Test + public void fastUUIDTest() { + String simpleUUID = IdUtil.fastSimpleUUID(); + Assert.assertEquals(32, simpleUUID.length()); + + String randomUUID = IdUtil.fastUUID(); + Assert.assertEquals(36, randomUUID.length()); + } + + /** + * UUID的性能测试 + */ + @Test + @Ignore + public void benchTest() { + TimeInterval timer = DateUtil.timer(); + for (int i = 0; i < 1000000; i++) { + IdUtil.simpleUUID(); + } + Console.log(timer.interval()); + + timer.restart(); + for (int i = 0; i < 1000000; i++) { + UUID.randomUUID().toString().replace("-", ""); + } + Console.log(timer.interval()); + } + + @Test + public void objectIdTest() { + String id = IdUtil.objectId(); + Assert.assertEquals(24, id.length()); + } + + @Test + public void createSnowflakeTest() { + Snowflake snowflake = IdUtil.createSnowflake(1, 1); + long id = snowflake.nextId(); + Assert.assertTrue(id > 0); + } + + @Test + public void snowflakeBenchTest() { + final Set set = new ConcurrentHashSet<>(); + final Snowflake snowflake = IdUtil.createSnowflake(1, 1); + + //线程数 + int threadCount = 100; + //每个线程生成的ID数 + final int idCountPerThread = 10000; + final CountDownLatch latch = new CountDownLatch(threadCount); + for(int i =0; i < threadCount; i++) { + ThreadUtil.execute(new Runnable() { + + @Override + public void run() { + for(int i =0; i < idCountPerThread; i++) { + long id = snowflake.nextId(); + set.add(id); +// Console.log("Add new id: {}", id); + } + latch.countDown(); + } + }); + } + + //等待全部线程结束 + try { + latch.await(); + } catch (InterruptedException e) { + throw new UtilException(e); + } + Assert.assertEquals(threadCount * idCountPerThread, set.size()); + } + + @Test + public void snowflakeBenchTest2() { + final Set set = new ConcurrentHashSet<>(); + + //线程数 + int threadCount = 100; + //每个线程生成的ID数 + final int idCountPerThread = 10000; + final CountDownLatch latch = new CountDownLatch(threadCount); + for(int i =0; i < threadCount; i++) { + ThreadUtil.execute(new Runnable() { + + @Override + public void run() { + for(int i =0; i < idCountPerThread; i++) { + long id = IdUtil.getSnowflake(1, 1).nextId(); + set.add(id); +// Console.log("Add new id: {}", id); + } + latch.countDown(); + } + }); + } + + //等待全部线程结束 + try { + latch.await(); + } catch (InterruptedException e) { + throw new UtilException(e); + } + Assert.assertEquals(threadCount * idCountPerThread, set.size()); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/util/IdcardUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/IdcardUtilTest.java new file mode 100644 index 000000000..aa95d068f --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/util/IdcardUtilTest.java @@ -0,0 +1,69 @@ +package cn.hutool.core.util; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; + +/** + * 身份证单元测试 + * + * @author Looly + * + */ +public class IdcardUtilTest { + private static final String ID_18 = "321083197812162119"; + private static final String ID_15 = "150102880730303"; + + @Test + public void isValidCardTest() { + boolean valid = IdcardUtil.isValidCard(ID_18); + Assert.assertTrue(valid); + + boolean valid15 = IdcardUtil.isValidCard(ID_15); + Assert.assertTrue(valid15); + + String idCard = "360198910283844"; + Assert.assertFalse(IdcardUtil.isValidCard(idCard)); + } + + @Test + public void convert15To18Test() { + String convert15To18 = IdcardUtil.convert15To18(ID_15); + Assert.assertEquals("150102198807303035", convert15To18); + + String convert15To18Second = IdcardUtil.convert15To18("330102200403064"); + Assert.assertEquals("33010219200403064x", convert15To18Second); + } + + @Test + public void getAgeByIdCardTest() { + DateTime date = DateUtil.parse("2017-04-10"); + + int age = IdcardUtil.getAgeByIdCard(ID_18, date); + Assert.assertEquals(age, 38); + + int age2 = IdcardUtil.getAgeByIdCard(ID_15, date); + Assert.assertEquals(age2, 28); + } + + @Test + public void getBirthByIdCardTest() { + String birth = IdcardUtil.getBirthByIdCard(ID_18); + Assert.assertEquals(birth, "19781216"); + + String birth2 = IdcardUtil.getBirthByIdCard(ID_15); + Assert.assertEquals(birth2, "19880730"); + } + + @Test + public void getProvinceByIdCardTest() { + String province = IdcardUtil.getProvinceByIdCard(ID_18); + Assert.assertEquals(province, "江苏"); + + String province2 = IdcardUtil.getProvinceByIdCard(ID_15); + Assert.assertEquals(province2, "内蒙古"); + } + +} diff --git a/hutool-core/src/test/java/cn/hutool/core/util/NumberUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/NumberUtilTest.java new file mode 100644 index 000000000..e9ea70faa --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/util/NumberUtilTest.java @@ -0,0 +1,232 @@ +package cn.hutool.core.util; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +import org.junit.Assert; +import org.junit.Test; + +/** + * {@link NumberUtil} 单元测试类 + * + * @author Looly + * + */ +public class NumberUtilTest { + + @Test + public void addTest() { + Float a = 3.15f; + Double b = 4.22; + double result = NumberUtil.add(a, b).doubleValue(); + Assert.assertEquals(7.37, result, 2); + } + + @Test + public void addTest2() { + double a = 3.15f; + double b = 4.22; + double result = NumberUtil.add(a, b); + Assert.assertEquals(7.37, result, 2); + } + + @Test + public void addTest3() { + float a = 3.15f; + double b = 4.22; + double result = NumberUtil.add(a, b, a, b).doubleValue(); + Assert.assertEquals(14.74, result, 2); + } + + @Test + public void addTest4() { + BigDecimal result = NumberUtil.add(new BigDecimal("133"), new BigDecimal("331")); + Assert.assertEquals(new BigDecimal("464"), result); + } + + @Test + public void isIntegerTest() { + Assert.assertTrue(NumberUtil.isInteger("-12")); + Assert.assertTrue(NumberUtil.isInteger("256")); + Assert.assertTrue(NumberUtil.isInteger("0256")); + Assert.assertTrue(NumberUtil.isInteger("0")); + Assert.assertFalse(NumberUtil.isInteger("23.4")); + } + + @Test + public void isLongTest() { + Assert.assertTrue(NumberUtil.isLong("-12")); + Assert.assertTrue(NumberUtil.isLong("256")); + Assert.assertTrue(NumberUtil.isLong("0256")); + Assert.assertTrue(NumberUtil.isLong("0")); + Assert.assertFalse(NumberUtil.isLong("23.4")); + } + + @Test + public void isNumberTest() { + Assert.assertTrue(NumberUtil.isNumber("28.55")); + Assert.assertTrue(NumberUtil.isNumber("0")); + } + + @Test + public void divTest() { + double result = NumberUtil.div(0, 1); + Assert.assertEquals(0.0, result, 0); + } + + @Test + public void roundTest() { + + // 四舍 + String round1 = NumberUtil.roundStr(2.674, 2); + String round2 = NumberUtil.roundStr("2.674", 2); + Assert.assertEquals("2.67", round1); + Assert.assertEquals("2.67", round2); + + // 五入 + String round3 = NumberUtil.roundStr(2.675, 2); + String round4 = NumberUtil.roundStr("2.675", 2); + Assert.assertEquals("2.68", round3); + Assert.assertEquals("2.68", round4); + + // 四舍六入五成双 + String round31 = NumberUtil.roundStr(4.245, 2, RoundingMode.HALF_EVEN); + String round41 = NumberUtil.roundStr("4.2451", 2, RoundingMode.HALF_EVEN); + Assert.assertEquals("4.24", round31); + Assert.assertEquals("4.25", round41); + + // 补0 + String round5 = NumberUtil.roundStr(2.6005, 2); + String round6 = NumberUtil.roundStr("2.6005", 2); + Assert.assertEquals("2.60", round5); + Assert.assertEquals("2.60", round6); + + // 补0 + String round7 = NumberUtil.roundStr(2.600, 2); + String round8 = NumberUtil.roundStr("2.600", 2); + Assert.assertEquals("2.60", round7); + Assert.assertEquals("2.60", round8); + } + + @Test + public void roundStrTest() { + String roundStr = NumberUtil.roundStr(2.647, 2); + Assert.assertEquals(roundStr, "2.65"); + } + + @Test + public void roundHalfEvenTest() { + String roundStr = NumberUtil.roundHalfEven(4.245, 2).toString(); + Assert.assertEquals(roundStr, "4.24"); + roundStr = NumberUtil.roundHalfEven(4.2450, 2).toString(); + Assert.assertEquals(roundStr, "4.24"); + roundStr = NumberUtil.roundHalfEven(4.2451, 2).toString(); + Assert.assertEquals(roundStr, "4.25"); + roundStr = NumberUtil.roundHalfEven(4.2250, 2).toString(); + Assert.assertEquals(roundStr, "4.22"); + + roundStr = NumberUtil.roundHalfEven(1.2050, 2).toString(); + Assert.assertEquals(roundStr, "1.20"); + roundStr = NumberUtil.roundHalfEven(1.2150, 2).toString(); + Assert.assertEquals(roundStr, "1.22"); + roundStr = NumberUtil.roundHalfEven(1.2250, 2).toString(); + Assert.assertEquals(roundStr, "1.22"); + roundStr = NumberUtil.roundHalfEven(1.2350, 2).toString(); + Assert.assertEquals(roundStr, "1.24"); + roundStr = NumberUtil.roundHalfEven(1.2450, 2).toString(); + Assert.assertEquals(roundStr, "1.24"); + roundStr = NumberUtil.roundHalfEven(1.2550, 2).toString(); + Assert.assertEquals(roundStr, "1.26"); + roundStr = NumberUtil.roundHalfEven(1.2650, 2).toString(); + Assert.assertEquals(roundStr, "1.26"); + roundStr = NumberUtil.roundHalfEven(1.2750, 2).toString(); + Assert.assertEquals(roundStr, "1.28"); + roundStr = NumberUtil.roundHalfEven(1.2850, 2).toString(); + Assert.assertEquals(roundStr, "1.28"); + roundStr = NumberUtil.roundHalfEven(1.2950, 2).toString(); + Assert.assertEquals(roundStr, "1.30"); + } + + @Test + public void decimalFormatTest() { + long c = 299792458;// 光速 + + String format = NumberUtil.decimalFormat(",###", c); + Assert.assertEquals("299,792,458", format); + } + + @Test + public void decimalFormatMoneyTest() { + double c = 299792400.543534534; + + String format = NumberUtil.decimalFormatMoney(c); + Assert.assertEquals("299,792,400.54", format); + + double value = 0.5; + String money = NumberUtil.decimalFormatMoney(value); + Assert.assertEquals("0.50", money); + } + + @Test + public void equalsTest() { + Assert.assertTrue(NumberUtil.equals(new BigDecimal("0.00"), BigDecimal.ZERO)); + } + + @Test + public void formatPercentTest() { + String str = NumberUtil.formatPercent(0.33543545, 2); + Assert.assertEquals("33.54%", str); + } + + @Test + public void toBigDecimalTest() { + double a = 3.14; + + BigDecimal bigDecimal = NumberUtil.toBigDecimal(a); + Assert.assertEquals("3.14", bigDecimal.toString()); + } + + @Test + public void maxTest() { + int max = NumberUtil.max(new int[]{5,4,3,6,1}); + Assert.assertEquals(6, max); + } + + @Test + public void minTest() { + int min = NumberUtil.min(new int[]{5,4,3,6,1}); + Assert.assertEquals(1, min); + } + + @Test + public void parseIntTest() { + int v1 = NumberUtil.parseInt("0xFF"); + Assert.assertEquals(255, v1); + int v2 = NumberUtil.parseInt("010"); + Assert.assertEquals(10, v2); + int v3 = NumberUtil.parseInt("10"); + Assert.assertEquals(10, v3); + int v4 = NumberUtil.parseInt(" "); + Assert.assertEquals(0, v4); + int v5 = NumberUtil.parseInt("10F"); + Assert.assertEquals(10, v5); + int v6 = NumberUtil.parseInt("22.4D"); + Assert.assertEquals(22, v6); + } + + @Test + public void parseLongTest() { + long v1 = NumberUtil.parseLong("0xFF"); + Assert.assertEquals(255L, v1); + long v2 = NumberUtil.parseLong("010"); + Assert.assertEquals(10L, v2); + long v3 = NumberUtil.parseLong("10"); + Assert.assertEquals(10L, v3); + long v4 = NumberUtil.parseLong(" "); + Assert.assertEquals(0L, v4); + long v5 = NumberUtil.parseLong("10F"); + Assert.assertEquals(10L, v5); + long v6 = NumberUtil.parseLong("22.4D"); + Assert.assertEquals(22L, v6); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/util/ObjectUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/ObjectUtilTest.java new file mode 100644 index 000000000..d9f3c627c --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/util/ObjectUtilTest.java @@ -0,0 +1,32 @@ +package cn.hutool.core.util; + +import java.util.ArrayList; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.clone.CloneSupport; +import cn.hutool.core.collection.CollUtil; + +public class ObjectUtilTest { + + @Test + public void cloneTest() { + Obj obj = new Obj(); + Obj obj2 = ObjectUtil.clone(obj); + Assert.assertEquals("OK", obj2.doSomeThing()); + } + + static class Obj extends CloneSupport { + public String doSomeThing() { + return "OK"; + } + } + + @Test + public void toStringTest() { + ArrayList strings = CollUtil.newArrayList("1", "2"); + String result = ObjectUtil.toString(strings); + Assert.assertEquals("[1, 2]", result); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/util/PageUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/PageUtilTest.java new file mode 100644 index 000000000..2158bce92 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/util/PageUtilTest.java @@ -0,0 +1,35 @@ +package cn.hutool.core.util; + +import org.junit.Assert; +import org.junit.Test; + +/** + * 分页单元测试 + * @author Looly + * + */ +public class PageUtilTest { + + @Test + public void transToStartEndTest(){ + int[] startEnd1 = PageUtil.transToStartEnd(1, 10); + Assert.assertEquals(0, startEnd1[0]); + Assert.assertEquals(10, startEnd1[1]); + + int[] startEnd2 = PageUtil.transToStartEnd(2, 10); + Assert.assertEquals(10, startEnd2[0]); + Assert.assertEquals(20, startEnd2[1]); + } + + @Test + public void totalPage(){ + int totalPage = PageUtil.totalPage(20, 3); + Assert.assertEquals(7, totalPage); + } + + @Test + public void rainbowTest() { + int[] rainbow = PageUtil.rainbow(5, 20, 6); + Assert.assertArrayEquals(new int[] {3, 4, 5, 6, 7, 8}, rainbow); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/util/PinyinUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/PinyinUtilTest.java new file mode 100644 index 000000000..3e495f23e --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/util/PinyinUtilTest.java @@ -0,0 +1,52 @@ +package cn.hutool.core.util; + +import org.junit.Assert; +import org.junit.Test; + +@SuppressWarnings("deprecation") +public class PinyinUtilTest { + + @Test + public void getFirstLetterTest() { + char firstLetter = PinyinUtil.getFirstLetter('你'); + Assert.assertEquals('n', firstLetter); + + firstLetter = PinyinUtil.getFirstLetter('我'); + Assert.assertEquals('w', firstLetter); + +// firstLetter = PinyinUtil.getFirstLetter('怡'); +// Console.log(firstLetter); +// Assert.assertEquals('y', firstLetter); + } + + @Test + public void getAllFirstLetterTest() { + String allFirstLetter = PinyinUtil.getAllFirstLetter("会当凌绝顶"); + Assert.assertEquals("hdljd", allFirstLetter); + + allFirstLetter = PinyinUtil.getAllFirstLetter("一览众山小"); + Assert.assertEquals("ylzsx", allFirstLetter); + } + + @Test + public void getAllFirstLetterTest2() { + String allFirstLetter = PinyinUtil.getAllFirstLetter("张三123"); + Assert.assertEquals("zs123", allFirstLetter); + } + + @Test + public void getPinyinTest() { + String pinYin = PinyinUtil.getPinYin("会当凌绝顶"); + Assert.assertEquals("huidanglingjueding", pinYin); + + pinYin = PinyinUtil.getPinYin("一览众山小"); + Assert.assertEquals("yilanzhongshanxiao", pinYin); + +// pinYin = PinyinUtil.getPinYin("怡"); +// Assert.assertEquals("yi", pinYin); + + String cnStr = "中文(进一步说明)-(OK)@gitee.com"; + pinYin = PinyinUtil.getPinYin(cnStr); + Assert.assertEquals("zhongwen(jinyibushuoming)-OK@gitee.com", pinYin); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/util/RandomUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/RandomUtilTest.java new file mode 100644 index 000000000..a56ce78a2 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/util/RandomUtilTest.java @@ -0,0 +1,39 @@ +package cn.hutool.core.util; + +import java.math.RoundingMode; +import java.util.List; +import java.util.Set; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Console; + +public class RandomUtilTest { + + @Test + public void randomEleSetTest(){ + Set set = RandomUtil.randomEleSet(CollectionUtil.newArrayList(1, 2, 3, 4, 5, 6), 2); + Assert.assertEquals(set.size(), 2); + } + + @Test + public void randomElesTest(){ + List result = RandomUtil.randomEles(CollectionUtil.newArrayList(1, 2, 3, 4, 5, 6), 2); + Assert.assertEquals(result.size(), 2); + } + + @Test + public void randomDoubleTest() { + double randomDouble = RandomUtil.randomDouble(0, 1, 0, RoundingMode.HALF_UP); + Assert.assertTrue(randomDouble <= 1); + } + + @Test + @Ignore + public void randomBooleanTest() { + Console.log(RandomUtil.randomBoolean()); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/util/ReUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/ReUtilTest.java new file mode 100644 index 000000000..2acdd8aee --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/util/ReUtilTest.java @@ -0,0 +1,139 @@ +package cn.hutool.core.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.func.Func1; + +public class ReUtilTest { + final String content = "ZZZaaabbbccc中文1234"; + + @Test + public void getTest() { + String resultGet = ReUtil.get("\\w{2}", content, 0); + Assert.assertEquals("ZZ", resultGet); + } + + @Test + public void extractMultiTest() { + // 抽取多个分组然后把它们拼接起来 + String resultExtractMulti = ReUtil.extractMulti("(\\w)aa(\\w)", content, "$1-$2"); + Assert.assertEquals("Z-a", resultExtractMulti); + } + + @Test + public void extractMultiTest2() { + // 抽取多个分组然后把它们拼接起来 + String resultExtractMulti = ReUtil.extractMulti("(\\w)(\\w)(\\w)(\\w)(\\w)(\\w)(\\w)(\\w)(\\w)(\\w)", content, "$1-$2-$3-$4-$5-$6-$7-$8-$9-$10"); + Assert.assertEquals("Z-Z-Z-a-a-a-b-b-b-c", resultExtractMulti); + } + + @Test + public void delFirstTest() { + // 删除第一个匹配到的内容 + String resultDelFirst = ReUtil.delFirst("(\\w)aa(\\w)", content); + Assert.assertEquals("ZZbbbccc中文1234", resultDelFirst); + } + + @Test + public void delAllTest() { + // 删除所有匹配到的内容 + String content = "发东方大厦eee![images]http://abc.com/2.gpg]好机会eee![images]http://abc.com/2.gpg]好机会"; + String resultDelAll = ReUtil.delAll("!\\[images\\][^\\u4e00-\\u9fa5\\\\s]*", content); + Assert.assertEquals("发东方大厦eee好机会eee好机会", resultDelAll); + } + + @Test + public void findAllTest() { + // 查找所有匹配文本 + List resultFindAll = ReUtil.findAll("\\w{2}", content, 0, new ArrayList()); + ArrayList expected = CollectionUtil.newArrayList("ZZ", "Za", "aa", "bb", "bc", "cc", "12", "34"); + Assert.assertEquals(expected, resultFindAll); + } + + @Test + public void getFirstNumberTest() { + // 找到匹配的第一个数字 + Integer resultGetFirstNumber = ReUtil.getFirstNumber(content); + Assert.assertEquals(Integer.valueOf(1234), resultGetFirstNumber); + } + + @Test + public void isMatchTest() { + // 给定字符串是否匹配给定正则 + boolean isMatch = ReUtil.isMatch("\\w+[\u4E00-\u9FFF]+\\d+", content); + Assert.assertTrue(isMatch); + } + + @Test + public void replaceAllTest() { + //通过正则查找到字符串,然后把匹配到的字符串加入到replacementTemplate中,$1表示分组1的字符串 + //此处把1234替换为 ->1234<- + String replaceAll = ReUtil.replaceAll(content, "(\\d+)", "->$1<-"); + Assert.assertEquals("ZZZaaabbbccc中文->1234<-", replaceAll); + } + + @Test + public void replaceAllTest2() { + //此处把1234替换为 ->1234<- + String replaceAll = ReUtil.replaceAll(this.content, "(\\d+)", new Func1() { + + @Override + public String call(Matcher parameters) { + return "->" + parameters.group(1) + "<-"; + } + }); + Assert.assertEquals("ZZZaaabbbccc中文->1234<-", replaceAll); + } + + @Test + public void replaceTest() { + String str = "AAABBCCCBBDDDBB"; + String replace = StrUtil.replace(str, 0, "BB", "22", false); + Assert.assertEquals("AAA22CCC22DDD22", replace); + + replace = StrUtil.replace(str, 3, "BB", "22", false); + Assert.assertEquals("AAA22CCC22DDD22", replace); + + replace = StrUtil.replace(str, 4, "BB", "22", false); + Assert.assertEquals("AAABBCCC22DDD22", replace); + + replace = StrUtil.replace(str, 4, "bb", "22", true); + Assert.assertEquals("AAABBCCC22DDD22", replace); + + replace = StrUtil.replace(str, 4, "bb", "", true); + Assert.assertEquals("AAABBCCCDDD", replace); + + replace = StrUtil.replace(str, 4, "bb", null, true); + Assert.assertEquals("AAABBCCCDDD", replace); + } + + @Test + public void escapeTest() { + //转义给定字符串,为正则相关的特殊符号转义 + String escape = ReUtil.escape("我有个$符号{}"); + Assert.assertEquals("我有个\\$符号\\{\\}", escape); + } + + @Test + public void getAllGroupsTest() { + //转义给定字符串,为正则相关的特殊符号转义 + Pattern pattern = Pattern.compile("(\\d+)-(\\d+)-(\\d+)"); + List allGroups = ReUtil.getAllGroups(pattern, "192-168-1-1"); + Assert.assertEquals("192-168-1", allGroups.get(0)); + Assert.assertEquals("192", allGroups.get(1)); + Assert.assertEquals("168", allGroups.get(2)); + Assert.assertEquals("1", allGroups.get(3)); + + allGroups = ReUtil.getAllGroups(pattern, "192-168-1-1", false); + Assert.assertEquals("192", allGroups.get(0)); + Assert.assertEquals("168", allGroups.get(1)); + Assert.assertEquals("1", allGroups.get(2)); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/util/ReflectUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/ReflectUtilTest.java new file mode 100644 index 000000000..385c22583 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/util/ReflectUtilTest.java @@ -0,0 +1,105 @@ +package cn.hutool.core.util; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.lang.Filter; +import cn.hutool.core.lang.test.bean.ExamInfoDict; +import cn.hutool.core.util.ClassUtilTest.TestSubClass; + +/** + * 反射工具类单元测试 + * + * @author Looly + * + */ +public class ReflectUtilTest { + + @Test + public void getMethodsTest() { + Method[] methods = ReflectUtil.getMethods(ExamInfoDict.class); + Assert.assertEquals(22, methods.length); + + //过滤器测试 + methods = ReflectUtil.getMethods(ExamInfoDict.class, new Filter() { + + @Override + public boolean accept(Method t) { + return Integer.class.equals(t.getReturnType()); + } + }); + + Assert.assertEquals(4, methods.length); + final Method method = methods[0]; + Assert.assertNotNull(method); + + //null过滤器测试 + methods = ReflectUtil.getMethods(ExamInfoDict.class, null); + + Assert.assertEquals(22, methods.length); + final Method method2 = methods[0]; + Assert.assertNotNull(method2); + } + + @Test + public void getMethodTest() { + Method method = ReflectUtil.getMethod(ExamInfoDict.class, "getId"); + Assert.assertEquals("getId", method.getName()); + Assert.assertEquals(0, method.getParameterTypes().length); + + method = ReflectUtil.getMethod(ExamInfoDict.class, "getId", Integer.class); + Assert.assertEquals("getId", method.getName()); + Assert.assertEquals(1, method.getParameterTypes().length); + } + + @Test + public void getMethodIgnoreCaseTest() { + Method method = ReflectUtil.getMethodIgnoreCase(ExamInfoDict.class, "getId"); + Assert.assertEquals("getId", method.getName()); + Assert.assertEquals(0, method.getParameterTypes().length); + + method = ReflectUtil.getMethodIgnoreCase(ExamInfoDict.class, "GetId"); + Assert.assertEquals("getId", method.getName()); + Assert.assertEquals(0, method.getParameterTypes().length); + + method = ReflectUtil.getMethodIgnoreCase(ExamInfoDict.class, "setanswerIs", Integer.class); + Assert.assertEquals("setAnswerIs", method.getName()); + Assert.assertEquals(1, method.getParameterTypes().length); + } + + @Test + public void getFieldTest() { + // 能够获取到父类字段 + Field privateField = ReflectUtil.getField(TestSubClass.class, "privateField"); + Assert.assertNotNull(privateField); + } + + @Test + public void setFieldTest() { + TestClass testClass = new TestClass(); + ReflectUtil.setFieldValue(testClass, "a", "111"); + Assert.assertEquals(111, testClass.getA()); + } + + @Test + public void invokeTest() { + TestClass testClass = new TestClass(); + ReflectUtil.invoke(testClass, "setA", 10); + Assert.assertEquals(10, testClass.getA()); + } + + static class TestClass { + private int a; + + public int getA() { + return a; + } + + public void setA(int a) { + this.a = a; + } + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/util/RuntimeUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/RuntimeUtilTest.java new file mode 100644 index 000000000..4e45a6384 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/util/RuntimeUtilTest.java @@ -0,0 +1,29 @@ +package cn.hutool.core.util; + +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.lang.Console; +import cn.hutool.core.util.RuntimeUtil; + +/** + * 命令行单元测试 + * @author looly + * + */ +public class RuntimeUtilTest { + + @Test + @Ignore + public void execTest() { + String str = RuntimeUtil.execForStr("ipconfig"); + Console.log(str); + } + + @Test + @Ignore + public void execCmdTest() { + String str = RuntimeUtil.execForStr("cmd /c dir"); + Console.log(str); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/util/StrUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/StrUtilTest.java new file mode 100644 index 000000000..dbaf1f254 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/util/StrUtilTest.java @@ -0,0 +1,409 @@ +package cn.hutool.core.util; + +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.lang.Dict; + +/** + * 字符串工具类单元测试 + * + * @author Looly + * + */ +public class StrUtilTest { + + @Test + public void isBlankTest() { + String blank = "   "; + Assert.assertTrue(StrUtil.isBlank(blank)); + } + + @Test + public void isBlankTest2() { + String blank = "\u202a"; + Assert.assertTrue(StrUtil.isBlank(blank)); + } + + @Test + public void trimTest() { + String blank = " 哈哈  "; + String trim = StrUtil.trim(blank); + Assert.assertEquals("哈哈", trim); + } + + @Test + public void cleanBlankTest() { + // 包含:制表符、英文空格、不间断空白符、全角空格 + String str = " 你 好 "; + String cleanBlank = StrUtil.cleanBlank(str); + Assert.assertEquals("你好", cleanBlank); + } + + @Test + public void cutTest() { + String str = "aaabbbcccdddaadfdfsdfsdf0"; + String[] cut = StrUtil.cut(str, 4); + Assert.assertArrayEquals(new String[] { "aaab", "bbcc", "cddd", "aadf", "dfsd", "fsdf", "0" }, cut); + } + + @Test + public void splitTest() { + String str = "a,b ,c,d,,e"; + List split = StrUtil.split(str, ',', -1, true, true); + // 测试空是否被去掉 + Assert.assertEquals(5, split.size()); + // 测试去掉两边空白符是否生效 + Assert.assertEquals("b", split.get(1)); + } + + @Test + public void splitToLongTest() { + String str = "1,2,3,4, 5"; + long[] longArray = StrUtil.splitToLong(str, ','); + Assert.assertArrayEquals(new long[] { 1, 2, 3, 4, 5 }, longArray); + + longArray = StrUtil.splitToLong(str, ","); + Assert.assertArrayEquals(new long[] { 1, 2, 3, 4, 5 }, longArray); + } + + @Test + public void splitToIntTest() { + String str = "1,2,3,4, 5"; + int[] intArray = StrUtil.splitToInt(str, ','); + Assert.assertArrayEquals(new int[] { 1, 2, 3, 4, 5 }, intArray); + + intArray = StrUtil.splitToInt(str, ","); + Assert.assertArrayEquals(new int[] { 1, 2, 3, 4, 5 }, intArray); + } + + @Test + public void formatTest() { + String template = "你好,我是{name},我的电话是:{phone}"; + String result = StrUtil.format(template, Dict.create().set("name", "张三").set("phone", "13888881111")); + Assert.assertEquals("你好,我是张三,我的电话是:13888881111", result); + + String result2 = StrUtil.format(template, Dict.create().set("name", "张三").set("phone", null)); + Assert.assertEquals("你好,我是张三,我的电话是:{phone}", result2); + } + + @Test + public void stripTest() { + String str = "abcd123"; + String strip = StrUtil.strip(str, "ab", "23"); + Assert.assertEquals("cd1", strip); + + str = "abcd123"; + strip = StrUtil.strip(str, "ab", ""); + Assert.assertEquals("cd123", strip); + + str = "abcd123"; + strip = StrUtil.strip(str, null, ""); + Assert.assertEquals("abcd123", strip); + + str = "abcd123"; + strip = StrUtil.strip(str, null, "567"); + Assert.assertEquals("abcd123", strip); + + Assert.assertEquals("", StrUtil.strip("a","a")); + Assert.assertEquals("", StrUtil.strip("a","a", "b")); + } + + @Test + public void stripIgnoreCaseTest() { + String str = "abcd123"; + String strip = StrUtil.stripIgnoreCase(str, "Ab", "23"); + Assert.assertEquals("cd1", strip); + + str = "abcd123"; + strip = StrUtil.stripIgnoreCase(str, "AB", ""); + Assert.assertEquals("cd123", strip); + + str = "abcd123"; + strip = StrUtil.stripIgnoreCase(str, "ab", ""); + Assert.assertEquals("cd123", strip); + + str = "abcd123"; + strip = StrUtil.stripIgnoreCase(str, null, ""); + Assert.assertEquals("abcd123", strip); + + str = "abcd123"; + strip = StrUtil.stripIgnoreCase(str, null, "567"); + Assert.assertEquals("abcd123", strip); + } + + @Test + public void indexOfIgnoreCaseTest() { + Assert.assertEquals(-1, StrUtil.indexOfIgnoreCase(null, "balabala", 0)); + Assert.assertEquals(-1, StrUtil.indexOfIgnoreCase("balabala", null, 0)); + Assert.assertEquals(0, StrUtil.indexOfIgnoreCase("", "", 0)); + Assert.assertEquals(0, StrUtil.indexOfIgnoreCase("aabaabaa", "A", 0)); + Assert.assertEquals(2, StrUtil.indexOfIgnoreCase("aabaabaa", "B", 0)); + Assert.assertEquals(1, StrUtil.indexOfIgnoreCase("aabaabaa", "AB", 0)); + Assert.assertEquals(5, StrUtil.indexOfIgnoreCase("aabaabaa", "B", 3)); + Assert.assertEquals(-1, StrUtil.indexOfIgnoreCase("aabaabaa", "B", 9)); + Assert.assertEquals(2, StrUtil.indexOfIgnoreCase("aabaabaa", "B", -1)); + Assert.assertEquals(2, StrUtil.indexOfIgnoreCase("aabaabaa", "", 2)); + Assert.assertEquals(-1, StrUtil.indexOfIgnoreCase("abc", "", 9)); + } + + @Test + public void lastIndexOfTest() { + String a = "aabbccddcc"; + int lastIndexOf = StrUtil.lastIndexOf(a, "c", 0, false); + Assert.assertEquals(-1, lastIndexOf); + } + + @Test + public void lastIndexOfIgnoreCaseTest() { + Assert.assertEquals(-1, StrUtil.lastIndexOfIgnoreCase(null, "balabala", 0)); + Assert.assertEquals(-1, StrUtil.lastIndexOfIgnoreCase("balabala", null)); + Assert.assertEquals(0, StrUtil.lastIndexOfIgnoreCase("", "")); + Assert.assertEquals(7, StrUtil.lastIndexOfIgnoreCase("aabaabaa", "A")); + Assert.assertEquals(5, StrUtil.lastIndexOfIgnoreCase("aabaabaa", "B")); + Assert.assertEquals(4, StrUtil.lastIndexOfIgnoreCase("aabaabaa", "AB")); + Assert.assertEquals(2, StrUtil.lastIndexOfIgnoreCase("aabaabaa", "B", 3)); + Assert.assertEquals(5, StrUtil.lastIndexOfIgnoreCase("aabaabaa", "B", 9)); + Assert.assertEquals(-1, StrUtil.lastIndexOfIgnoreCase("aabaabaa", "B", -1)); + Assert.assertEquals(2, StrUtil.lastIndexOfIgnoreCase("aabaabaa", "", 2)); + Assert.assertEquals(3, StrUtil.lastIndexOfIgnoreCase("abc", "", 9)); + } + + @Test + public void replaceTest() { + String string = StrUtil.replace("aabbccdd", 2, 6, '*'); + Assert.assertEquals("aa****dd", string); + string = StrUtil.replace("aabbccdd", 2, 12, '*'); + Assert.assertEquals("aa******", string); + } + + @Test + public void replaceTest2() { + String result = StrUtil.replace("123", "2", "3"); + Assert.assertEquals("133", result); + } + + @Test + public void replaceTest3() { + String result = StrUtil.replace(",abcdef,", ",", "|"); + Assert.assertEquals("|abcdef|", result); + } + + @Test + public void replaceTest4() { + String a = "1039"; + String result = StrUtil.padPre(a,8,"0"); //在字符串1039前补4个0 + Assert.assertEquals("00001039", result); + } + + @Test + public void upperFirstTest() { + StringBuilder sb = new StringBuilder("KEY"); + String s = StrUtil.upperFirst(sb); + Assert.assertEquals(s, sb.toString()); + } + + @Test + public void lowerFirstTest() { + StringBuilder sb = new StringBuilder("KEY"); + String s = StrUtil.lowerFirst(sb); + Assert.assertEquals("kEY", s); + } + + @Test + public void subTest() { + String a = "abcderghigh"; + String pre = StrUtil.sub(a, -5, a.length()); + Assert.assertEquals("ghigh", pre); + } + + @Test + public void subBeforeTest() { + String a = "abcderghigh"; + String pre = StrUtil.subBefore(a, "d", false); + Assert.assertEquals("abc", pre); + pre = StrUtil.subBefore(a, 'd', false); + Assert.assertEquals("abc", pre); + pre = StrUtil.subBefore(a, 'a', false); + Assert.assertEquals("", pre); + + //找不到返回原串 + pre = StrUtil.subBefore(a, 'k', false); + Assert.assertEquals(a, pre); + pre = StrUtil.subBefore(a, 'k', true); + Assert.assertEquals(a, pre); + } + + @Test + public void subAfterTest() { + String a = "abcderghigh"; + String pre = StrUtil.subAfter(a, "d", false); + Assert.assertEquals("erghigh", pre); + pre = StrUtil.subAfter(a, 'd', false); + Assert.assertEquals("erghigh", pre); + pre = StrUtil.subAfter(a, 'h', true); + Assert.assertEquals("", pre); + + //找不到字符返回空串 + pre = StrUtil.subAfter(a, 'k', false); + Assert.assertEquals("", pre); + pre = StrUtil.subAfter(a, 'k', true); + Assert.assertEquals("", pre); + } + + @Test + public void subSufByLengthTest() { + Assert.assertEquals("cde", StrUtil.subSufByLength("abcde", 3)); + Assert.assertEquals("", StrUtil.subSufByLength("abcde", -1)); + Assert.assertEquals("", StrUtil.subSufByLength("abcde", 0)); + Assert.assertEquals("abcde", StrUtil.subSufByLength("abcde", 5)); + Assert.assertEquals("abcde", StrUtil.subSufByLength("abcde", 10)); + } + + @Test + public void repeatAndJoinTest() { + String repeatAndJoin = StrUtil.repeatAndJoin("?", 5, ","); + Assert.assertEquals("?,?,?,?,?", repeatAndJoin); + + repeatAndJoin = StrUtil.repeatAndJoin("?", 0, ","); + Assert.assertEquals("", repeatAndJoin); + + repeatAndJoin = StrUtil.repeatAndJoin("?", 5, null); + Assert.assertEquals("?????", repeatAndJoin); + } + + @Test + public void moveTest() { + String str = "aaaaaaa22222bbbbbbb"; + String result = StrUtil.move(str, 7, 12, -3); + Assert.assertEquals("aaaa22222aaabbbbbbb", result); + result = StrUtil.move(str, 7, 12, -4); + Assert.assertEquals("aaa22222aaaabbbbbbb", result); + result = StrUtil.move(str, 7, 12, -7); + Assert.assertEquals("22222aaaaaaabbbbbbb", result); + result = StrUtil.move(str, 7, 12, -20); + Assert.assertEquals("aaaaaa22222abbbbbbb", result); + + result = StrUtil.move(str, 7, 12, 3); + Assert.assertEquals("aaaaaaabbb22222bbbb", result); + result = StrUtil.move(str, 7, 12, 7); + Assert.assertEquals("aaaaaaabbbbbbb22222", result); + result = StrUtil.move(str, 7, 12, 20); + Assert.assertEquals("aaaaaaab22222bbbbbb", result); + + result = StrUtil.move(str, 7, 12, 0); + Assert.assertEquals("aaaaaaa22222bbbbbbb", result); + } + + @Test + public void removePrefixIgnorecaseTest() { + String a = "aaabbb"; + String prefix = "aaa"; + Assert.assertEquals("bbb", StrUtil.removePrefixIgnoreCase(a, prefix)); + + prefix = "AAA"; + Assert.assertEquals("bbb", StrUtil.removePrefixIgnoreCase(a, prefix)); + + prefix = "AAABBB"; + Assert.assertEquals("", StrUtil.removePrefixIgnoreCase(a, prefix)); + } + + @Test + public void maxLengthTest() { + String text = "我是一段正文,很长的正文,需要截取的正文"; + String str = StrUtil.maxLength(text, 5); + Assert.assertEquals("我是一段正...", str); + str = StrUtil.maxLength(text, 21); + Assert.assertEquals(text, str); + str = StrUtil.maxLength(text, 50); + Assert.assertEquals(text, str); + } + + @Test + public void toCamelCaseTest() { + String str = "Table_Test_Of_day"; + String result = StrUtil.toCamelCase(str); + Assert.assertEquals("tableTestOfDay", result); + + String str1 = "TableTestOfDay"; + String result1 = StrUtil.toCamelCase(str1); + Assert.assertEquals("TableTestOfDay", result1); + } + + @Test + public void toUnderLineCaseTest() { + String str = "Table_Test_Of_day"; + String result = StrUtil.toUnderlineCase(str); + Assert.assertEquals("table_test_of_day", result); + + String str1 = "_Table_Test_Of_day_"; + String result1 = StrUtil.toUnderlineCase(str1); + Assert.assertEquals("_table_test_of_day_", result1); + + String str2 = "_Table_Test_Of_DAY_"; + String result2 = StrUtil.toUnderlineCase(str2); + Assert.assertEquals("_table_test_of_DAY_", result2); + + String str3 = "_TableTestOfDAYtoday"; + String result3 = StrUtil.toUnderlineCase(str3); + Assert.assertEquals("_table_test_of_DAY_today", result3); + + String str4 = "HelloWorld_test"; + String result4 = StrUtil.toUnderlineCase(str4); + Assert.assertEquals("hello_world_test", result4); + } + + @Test + public void containsAnyTest() { + //字符 + boolean containsAny = StrUtil.containsAny("aaabbbccc", 'a', 'd'); + Assert.assertTrue(containsAny); + containsAny = StrUtil.containsAny("aaabbbccc", 'e', 'd'); + Assert.assertFalse(containsAny); + containsAny = StrUtil.containsAny("aaabbbccc", 'd', 'c'); + Assert.assertTrue(containsAny); + + //字符串 + containsAny = StrUtil.containsAny("aaabbbccc", "a", "d"); + Assert.assertTrue(containsAny); + containsAny = StrUtil.containsAny("aaabbbccc", "e", "d"); + Assert.assertFalse(containsAny); + containsAny = StrUtil.containsAny("aaabbbccc", "d", "c"); + Assert.assertTrue(containsAny); + } + + @Test + public void centerTest() { + Assert.assertNull(StrUtil.center(null, 10)); + Assert.assertEquals(" ", StrUtil.center("", 4)); + Assert.assertEquals("ab", StrUtil.center("ab", -1)); + Assert.assertEquals(" ab ", StrUtil.center("ab", 4)); + Assert.assertEquals("abcd", StrUtil.center("abcd", 2)); + Assert.assertEquals(" a ", StrUtil.center("a", 4)); + } + + @Test + public void padPreTest() { + Assert.assertNull(StrUtil.padPre(null, 10, ' ')); + Assert.assertEquals("001", StrUtil.padPre("1", 3, '0')); + Assert.assertEquals("12", StrUtil.padPre("123", 2, '0')); + + Assert.assertNull(StrUtil.padPre(null, 10, "AA")); + Assert.assertEquals("AB1", StrUtil.padPre("1", 3, "ABC")); + Assert.assertEquals("12", StrUtil.padPre("123", 2, "ABC")); + } + + @Test + public void padAfterTest() { + Assert.assertNull(StrUtil.padAfter(null, 10, ' ')); + Assert.assertEquals("100", StrUtil.padAfter("1", 3, '0')); + Assert.assertEquals("23", StrUtil.padAfter("123", 2, '0')); + + Assert.assertNull(StrUtil.padAfter(null, 10, "ABC")); + Assert.assertEquals("1AB", StrUtil.padAfter("1", 3, "ABC")); + Assert.assertEquals("23", StrUtil.padAfter("123", 2, "ABC")); + } + +} diff --git a/hutool-core/src/test/java/cn/hutool/core/util/TypeUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/TypeUtilTest.java new file mode 100644 index 000000000..6b7df700d --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/util/TypeUtilTest.java @@ -0,0 +1,42 @@ +package cn.hutool.core.util; + +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +public class TypeUtilTest { + + @Test + public void getEleTypeTest() { + Method method = ReflectUtil.getMethod(TestClass.class, "getList"); + Type type = TypeUtil.getReturnType(method); + Assert.assertEquals("java.util.List", type.toString()); + + Type type2 = TypeUtil.getTypeArgument(type); + Assert.assertEquals(String.class, type2); + } + + @Test + public void getParamTypeTest() { + Method method = ReflectUtil.getMethod(TestClass.class, "intTest", Integer.class); + Type type = TypeUtil.getParamType(method, 0); + Assert.assertEquals(Integer.class, type); + + Type returnType = TypeUtil.getReturnType(method); + Assert.assertEquals(Integer.class, returnType); + } + + public static class TestClass { + public List getList(){ + return new ArrayList<>(); + } + + public Integer intTest(Integer integer) { + return 1; + } + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/util/URLUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/URLUtilTest.java new file mode 100644 index 000000000..741741e47 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/util/URLUtilTest.java @@ -0,0 +1,64 @@ +package cn.hutool.core.util; + +import org.junit.Assert; +import org.junit.Test; + +/** + * URLUtil单元测试 + * + * @author looly + * + */ +public class URLUtilTest { + + @Test + public void normalizeTest() { + String url = "http://www.hutool.cn//aaa/bbb"; + String normalize = URLUtil.normalize(url); + Assert.assertEquals("http://www.hutool.cn/aaa/bbb", normalize); + + url = "www.hutool.cn//aaa/bbb"; + normalize = URLUtil.normalize(url); + Assert.assertEquals("http://www.hutool.cn/aaa/bbb", normalize); + } + + @Test + public void normalizeTest2() { + String url = "http://www.hutool.cn//aaa/\\bbb?a=1&b=2"; + String normalize = URLUtil.normalize(url); + Assert.assertEquals("http://www.hutool.cn/aaa/bbb?a=1&b=2", normalize); + + url = "www.hutool.cn//aaa/bbb?a=1&b=2"; + normalize = URLUtil.normalize(url); + Assert.assertEquals("http://www.hutool.cn/aaa/bbb?a=1&b=2", normalize); + } + + @Test + public void normalizeTest3() { + String url = "http://www.hutool.cn//aaa/\\bbb?a=1&b=2"; + String normalize = URLUtil.normalize(url, true); + Assert.assertEquals("http://www.hutool.cn/aaa/bbb?a=1&b=2", normalize); + + url = "www.hutool.cn//aaa/bbb?a=1&b=2"; + normalize = URLUtil.normalize(url, true); + Assert.assertEquals("http://www.hutool.cn/aaa/bbb?a=1&b=2", normalize); + } + + @Test + public void formatTest() { + String url = "//www.hutool.cn//aaa/\\bbb?a=1&b=2"; + String normalize = URLUtil.normalize(url); + Assert.assertEquals("http://www.hutool.cn/aaa/bbb?a=1&b=2", normalize); + } + + @Test + public void encodeTest() { + String body = "366466 - 副本.jpg"; + String encode = URLUtil.encode(body); + Assert.assertEquals("366466%20-%20%E5%89%AF%E6%9C%AC.jpg", encode); + Assert.assertEquals(body, URLUtil.decode(encode)); + + String encode2 = URLUtil.encodeQuery(body); + Assert.assertEquals("366466+-+%E5%89%AF%E6%9C%AC.jpg", encode2); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/util/XmlUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/XmlUtilTest.java new file mode 100644 index 000000000..550dfcf0a --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/util/XmlUtilTest.java @@ -0,0 +1,119 @@ +package cn.hutool.core.util; + +import java.util.LinkedHashMap; +import java.util.Map; + +import javax.xml.xpath.XPathConstants; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; +import org.w3c.dom.Document; + +import cn.hutool.core.map.MapBuilder; +import cn.hutool.core.map.MapUtil; + +/** + * {@link XmlUtil} 工具类 + * + * @author Looly + * + */ +public class XmlUtilTest { + + @Test + public void buildTest() { + } + + @Test + public void parseTest() { + String result = ""// + + ""// + + "Success"// + + "ok"// + + "1490"// + + "885"// + + "1"// + + ""; + Document docResult = XmlUtil.parseXml(result); + String elementText = XmlUtil.elementText(docResult.getDocumentElement(), "returnstatus"); + Assert.assertEquals("Success", elementText); + } + + @Test + @Ignore + public void writeTest() { + String result = ""// + + ""// + + "Success(成功)"// + + "ok"// + + "1490"// + + "885"// + + "1"// + + ""; + Document docResult = XmlUtil.parseXml(result); + XmlUtil.toFile(docResult, "e:/aaa.xml", "utf-8"); + } + + @Test + public void xpathTest() { + String result = ""// + + ""// + + "Success(成功)"// + + "ok"// + + "1490"// + + "885"// + + "1"// + + ""; + Document docResult = XmlUtil.parseXml(result); + Object value = XmlUtil.getByXPath("//returnsms/message", docResult, XPathConstants.STRING); + Assert.assertEquals("ok", value); + } + + @Test + public void xmlToMapTest() { + String xml = ""// + + ""// + + "Success"// + + "ok"// + + "1490"// + + "885"// + + "1"// + + ""; + Map map = XmlUtil.xmlToMap(xml); + + Assert.assertEquals(5, map.size()); + Assert.assertEquals("Success", map.get("returnstatus")); + Assert.assertEquals("ok", map.get("message")); + Assert.assertEquals("1490", map.get("remainpoint")); + Assert.assertEquals("885", map.get("taskID")); + Assert.assertEquals("1", map.get("successCounts")); + } + + @Test + public void mapToXmlTest() { + Map map = MapBuilder.create(new LinkedHashMap())// + .put("name", "张三")// + .put("age", 12)// + .put("game", MapUtil.builder(new LinkedHashMap()).put("昵称", "Looly").put("level", 14).build())// + .build(); + Document doc = XmlUtil.mapToXml(map, "user"); + // Console.log(XmlUtil.toStr(doc, false)); + Assert.assertEquals(""// + + ""// + + "张三"// + + "12"// + + ""// + + "<昵称>Looly"// + + "14"// + + ""// + + "", // + XmlUtil.toStr(doc, false)); + } + + @Test + public void readTest() { + Document doc = XmlUtil.readXML("test.xml"); + Assert.assertNotNull(doc); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/util/ZipUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/ZipUtilTest.java new file mode 100644 index 000000000..4cb606b35 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/util/ZipUtilTest.java @@ -0,0 +1,95 @@ +package cn.hutool.core.util; + +import java.io.File; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.lang.Console; + +/** + * {@link ZipUtil}单元测试 + * @author Looly + * + */ +public class ZipUtilTest { + + + @Test + @Ignore + public void zipDirTest() { + ZipUtil.zip(new File("e:/picTest/picSubTest")); + } + + @Test + @Ignore + public void unzipTest() { + File unzip = ZipUtil.unzip("E:\\aaa\\RongGenetor V1.0.0.zip", "e:\\aaa"); + Console.log(unzip); + File unzip2 = ZipUtil.unzip("E:\\aaa\\RongGenetor V1.0.0.zip", "e:\\aaa"); + Console.log(unzip2); + } + + @Test + @Ignore + public void unzipTest2() { + File unzip = ZipUtil.unzip("f:/test/各种资源.zip", "f:/test/各种资源", CharsetUtil.CHARSET_GBK); + Console.log(unzip); + } + + @Test + @Ignore + public void unzipFromStreamTest() { + File unzip = ZipUtil.unzip(FileUtil.getInputStream("e:/test/antlr.zip"), FileUtil.file("e:/test/"), CharsetUtil.CHARSET_UTF_8); + Console.log(unzip); + } + + @Test + @Ignore + public void unzipChineseTest() { + ZipUtil.unzip("d:/测试.zip"); + } + + @Test + @Ignore + public void unzipFileBytesTest() { + byte[] fileBytes = ZipUtil.unzipFileBytes(FileUtil.file("e:/02 电力相关设备及服务2-241-.zip"), CharsetUtil.CHARSET_GBK, "images/CE-EP-HY-MH01-ES-0001.jpg"); + Assert.assertNotNull(fileBytes); + } + + @Test + public void gzipTest() { + String data = "我是一个需要压缩的很长很长的字符串"; + byte[] bytes = StrUtil.utf8Bytes(data); + byte[] gzip = ZipUtil.gzip(bytes); + + //保证gzip长度正常 + Assert.assertEquals(68, gzip.length); + + byte[] unGzip = ZipUtil.unGzip(gzip); + //保证正常还原 + Assert.assertEquals(data, StrUtil.utf8Str(unGzip)); + } + + @Test + public void zlibTest() { + String data = "我是一个需要压缩的很长很长的字符串"; + byte[] bytes = StrUtil.utf8Bytes(data); + byte[] gzip = ZipUtil.zlib(bytes, 0); + + //保证zlib长度正常 + Assert.assertEquals(62, gzip.length); + byte[] unGzip = ZipUtil.unZlib(gzip); + //保证正常还原 + Assert.assertEquals(data, StrUtil.utf8Str(unGzip)); + + gzip = ZipUtil.zlib(bytes, 9); + //保证zlib长度正常 + Assert.assertEquals(56, gzip.length); + byte[] unGzip2 = ZipUtil.unZlib(gzip); + //保证正常还原 + Assert.assertEquals(data, StrUtil.utf8Str(unGzip2)); + } +} diff --git a/hutool-core/src/test/resources/hutool.jpg b/hutool-core/src/test/resources/hutool.jpg new file mode 100644 index 0000000000000000000000000000000000000000..09fe8b157de8e55a721760d0ca72dacd3f4299ea GIT binary patch literal 22807 zcmeIa2|SeF_c;EHvG04xmbJ3)yD37H2oZ`HV>e?NYZxgikt|WEP$H?cNl`?$RuM*I zC%c9$gE8~JL)-iF>9c)*|L^bh`n_JChv#|DJ@?#u&pG$p?cDnerIXSRaax#~n?f)c z3_1e-Aj$yKL6Zm{PYANKgtkKv#0W9KxFI-zVBime2|;x0Fa){7HvfcuVOzh`&;S}0 z2s|MXa0kOw*I`}&pF8OT(SA=`0Nx)U_QD7VlI8sI-sypHM~Pc|W3ZSIZ%m-Lv9b78 z2OqQtCOl*t1gR>kXzD6!>Z+=Xt7z#etLv((KoC3+_KUYXnjiVY#q;1lV2A+9qF(ec z_;*_R-{pXb{*nVG^Sc~0upfEA6ls3u%_y;v9kd&~fkrmoluk-FWDGIV(=*W1F)}bP zFflPQv+}aBvaqlUa`SNTiU^5`iUVJGuUO-%okTR49hi!pqxL|ND7^Myp1KiSqdZt$O z_X|b?r=_E3U}R!u0R&~75Dg3tr=f+@(b0m6g2jM%h?a|vdyC3WdY=7m3=%=Ss>jmu z86}PG*YMf25Tw-HgX5W)`8Np&3Q5bzZr!$BT|-k#TSwP;mx-y_ZgUG;y94$P2OXV| z9w<*QZy(XEWY*N;iIz0W2HdDJ3msO-fRkFs^OM$V=C}|(a}#7=weI!o$33*#eRozXVriRGY+}{oyaSg#&#HDp1%)APfptD8{FN8&U!y%n&vSe(k#Bh)29v#sdH+3 ztQQb)%JDMVt062E6XnXjGtA%X>d2Lp7n@m=%_SLfi_~E$$P$;ar9Q7pFYnChbS)jZ znd(u7g*&<{f0yL8>Izc#B{o-#=1|))E5pDGkgW!=V6G5<)lCaWHsP~xPG0)?M5u#z{V<2pmWb~3pKcTnY;2Y*9KjcUWt4PUwssd%hA0ef4tz+5II7Y z94~i@;q@%1&mp$#i(#^7#WD_g`HVRZX^eIZABsrVN-(#t?9JmC=*b<~!4q*&F!a?K z8u<3}O9JradIC0)R4}2*Ym{6 z`{(l=GBU1d2inP+H|Lh#_;i2nr0H4srtH&v56fet#DWfsox(bX z@(3?>M-Okd$Vhp4bCk4DsbW{Kj@WJLw7vq*{2o$dAAL!)a38Bcr;a zON_&!FIFzMr}(|BMqlW0)-9-up6J=!*NInYMd45pC+dgA^jon5 zw?0#y{jBhAb=&rE`5mLVi>dQ>IJgdeb{(Is_s{ukA&`8mf${4?9{lBFEq;D(li*__ zH{|Je4nbin_Rk?+YnaI&A2`Y<$}|_}ueptyqB5jyWNt|-y*?Im{uq*fyw~G&*F$*l zq{eRy+bn^>K4`2}C^j$@3y{>w7-SuSwROi}{MRYW(O47;9U4H5pu+3#055Uy=2%Z_ z>k~nxF!I4t(@>)~@C%ZZm82@$i_ol+yfKpz5~-w4X$5i!RWuk8TtP` zol&rd-2pV#RMN(uTAM#FBYzL^KjCdc{ITnJ>j?iH2cf#SjTFpbC?pmWyvq&iMwMWl zNoz0bkRNtHg{hR_O=TO}i}Kva=5LC%1GDZw6>E$1-@sc3BX>Abb6CGPk-?Zi2X7Rp zTws$vXs;jYW2Z*g0G^C6SS%*MAA|P#o`m&B43&l7@N7TuHa=e7zelkAhyc9(q^GtH z`1A%3&=EK{usWI_@AZ}=vmVF(y-@H%#6cVO#o$gI_<(bb~mKt=?P-|!4s@3zyF{9j>LlXVMpbn9^d%?)mb^?>=V-^ya}3dV#6 z{sl#k3HI^w`Psjk*iv)1TE~Gr*xW*~7&8n}t7a90tKw#b=redfu<{QJp z|F-<_&|v?+3@hu(|0~8a#Otp^551c|*3QlAFI4PEl)t|nDgtXBvfIvb?~ksI@dx@Z z#7y3p;HaJcK3+dG#jzp$?jLBN00yuwbqihZdYQsd!Pq~L+y6lSm7K-h%NXO23I0vF zTpRftnf;8Q>UbXpP1Q9M783{t?GV)OeH*hs=mh@`%j&+~Y5pCa1yr&3-;%Gl)UDKl zAXaeGAilSQbv)JUvSGq>>tQMd#K;34)W(qqZe;-WLlCuFVpxI%AQR<@KjTmgcE|%!hi#WhK_-rkq*wPw!Q+V-NLOxw{yQ6k3`ThReE0FB`}hd z;w!Vk+`|(uKZXu{GnLIO zNO*L?DffZk_;h1iK4ggctTa890R|b3+E&2C^-Ufo2Q;y$ z!JjPrV-940u|WBY<6*%r-CTN`^l&6C1!`uYy|7kXm%DoId5~2uLs`wl;E=GhEjugu zt60&=IKo$7^FZ8?cAD4Tc+-o*+=v<7o|Q8c=(NA#O3(Z1fjdSApRzJs+ z_{8%d&oYXJ=1!$4`J#0k*O=YOJpbd=H(DWscV*gj?xIqhjl>>4cbaq5gq=| zu2inE=c6iRWKj+8DxY_U2OQQS>wKj^udCNCP@ulh_o(|>F##2oLSl!nSiU>TL3g*Q z>r3UzJC`&Hq+xi60*!4-PIn^c4-xRH!rfL_F(zm?QE~b1Wa<3;mQ8)@t-Ylyqqx0;z)QKB4#X_-@b!%4d z-jz?vz!hFqG;DHIh)E5x8oM)cH>~}0OxT0hRkuA2iz>=fEktXb2DI$dL)_lPtz2Ei zFXR+uU%B3PG;}sJ?EW3{$M@~MG12!^R!TGR+HmA8KiEu+Q(swiC-QQtD&mG){%Y=g z%j>2!{5Dxv*R@TXNrzlB(pIexhi0AYf*Y7UZ9#ai=)HMdqI2Qt!7SC=MQ7yYX*ov` z<_atJ^p)duwxokGhQ)+N3e=Nuf8oqZ3~tt#Ts%>(Eq5W8h?sZ(H2g~c85172VqRfo z)f~|IY2Q6z_nw-Z-hFuGDbcHaR^KvptL>Y@jPKvS?0DGA?{ahD=#t;z`JA%eW_SE} z^<0<|X%Aw;=*tC@7`x4T={?@OqCm+j7L{(FV)-#sDk-I1C$v+(cp%zRZI>2rJ>Oj7 zfEZQpQM|1_ROp(1izhp7dWA1HL7x>ni2T7?C^>i zd{eO+R;6P)DGI5K=2ki<+d^~$$ccL{Pjnh9-Wh&UPUbEb`$mDX>TfJ$tnj(cz9e6( zKaY38hi+b+EW6{A+RH|Pd|M{(gy?#z9iG3m2%lw*vM^u3tnlT|Hj)d=Za;_x25Iz= ztWAO1I?B>*DJ>hu9vR+7uKIc#(;OhL-H4cm^BJKpVz+& zczoFFn?koK1Jk!QH_aQ^bwKBv=C zg(vOC{of-G;iJOwdScv+2+-9GgF2JXnxCmW%Z8m{%_W3EJx9wUkvU}FaU1h}V*AE4 zRXh4F?CcEJEWFY>ex;*&rFSIcj=KkH$l)*>dx$@~_=S&tz|dlyn52pj*9JY@k%pCB zLZb%{L2CJaz^@tuBGPjypEhRrTfhhpLAKO!o;pF`fObM2U<&RIp&)U{8cfPD>yz@3 zbtn)}aQ_Pmaj+#qU2tx!qG&hf`V8xnYES}YfV0pWiw)FOQbLC)x>0w36p@$!r3kk` zB^5JY3+7~triRa*#kPOM~u3c-X1BT*({phU?Zg@s^kP`|YE z$HM-a9FYG^9Qb2r@XzJ{j9xb$YKeim#=$5z;25#PVElhn^)DaX@B53tk)wbc1!W*X z-EUM-)>Kecx7*lnRMnJI*49;4-XOIEcH{y4E&qU6UF9#t7!M!M$bU)dfzy?`I(GYnfQpTzwm)#KH$hue(D&d**xyHd-CT9ukW?R{ zfdrCj4JZ$Z?`WStX!PIH$VRr|!1MJ_?PP;21mlSfcMC@C^a9%d7gnhB&jko^3;P%B zyK5+Gskpf-D7&evC}?=7YboexxoapW>v$-;D{CohXnOt>{~xgb%X0s7_P;9@>FtL0 zLV<2tN#b7!gv6k!>+WAd1-b|Rht?qxP+VqvE!_d!$rLXjd#`nEJ z?%L3;DYeqpyLsI|wy%HFKtaE+>+wJCu{O%z@RR)?^jI6#w4sCFhUWD#e_b(P&<6pa zl^+uh5c)AS_}_g0w~+rxyZ=?!f7SILY2ZI1{;zfYS6%;+2L2=B|614oU3LBPIiS!U z2D0I@2t&#s>p`$GakaLwGBw|8vhEyV-M!ZbjiKR$pa5`EfVxxx#~j5O-h&NACa`}A zwhrBpA%T{*rqsg}5Lm{DQ`a+8_}8`Av9YuHQDL{WxcD#pzXfq21B1bl0hl$QR`URU z0|1`@uzxrXhtwi`<04Ij|puz#%24E@w&;TC*Q}?WS15j>Ydy|&B$0UVCA-w^t3}Dt^ zI~(8vqSXgGPpn?Q!S27oSg@xDXdz=vVC4EPm-trXHgVuF(H7s03in50u?p6}5#$!^ z0lYK;fo|wX2-=XDnhN6nyS3svdgJj=BY&g&XCSq1H{!qTU#~M5|Jz@vKlcmgei4Go6Cg;S>laQc5BS+mK+xmn-{j$^+RJYqBp(z~k*d(2*S{qAN%@a~ z-^x>@miKd=Exv1GQ&gO4)kv_(85%4e0{n0&afQD%;{S2N-`M(%4|y=_M+KvRlSAAd zSQ$7_0;(JB;X_^giTj}c;tlvMhyP)+-*TXW8+Hu#7KGHuFO8>9-Iq4+{i8 zbQQ$Fe%9MwCVSuzg&+s9(G9x?Fo<7&{^dcF3W78tK3?Kfw6TqyI5IRij0$gT%z^`4 z%)rsg3kgD^kR-GfQh-z;El3aA0hvG+kQHPHIYKTF61ZFgpkOE*iiVCsiO?D79CRMK z1m!{nP%(5DdH|I}&!8Hp0eS<@)we_4P(L&RO@b2@i_j`K7tH`;gYm!wVPY_8m>f(M zrVTTMnZWkKY+#PCBQP%*8WswRh8>5Ufu+MP!SZ3nu=}v5uo~DaSPSeEYydV6n}e;; zz-d@%cxgmvWN4IVv}tzI?4hxvIZWe46GRh5bAsk9%>|lkG`DFU)6~$sp=qb-r{Ea zjIN%poo<9~k)EEOpI(+;i+(q~BfT&EQTk;1EA*xGFX&t7hv*l;$$UWuIR*m;D+V_P zEJGs0MTXl9)eNl+BMd}F7Dh2fRYp@rCq^`5JYyE)EygOwR>o1rRVGd*X(nAJD<%)7 zNTyV#0;US4W~LFQRc0<`S!M%fJ7!D#++`Pv79-aPdV|N3tT*0 zDqL1v0bI#kx42$$jd3$@%W#`;dvKrNzRvxUdx!_lBgtdTgXB5RQ@~TtGr~*HE6Z!n z>&ttF_YUuSUIHICpE{pCUj$zc-*dixemK7jzd64@e=7e2{to_?O(L6iZbEH3z3J|z z)=f(Sn+0|VpahZxN(I^lRs_WbO$7Y}(*(-}dxU6(whQeS3KzO6R4+8OnRm0^X5{9i z%?~zr3DXE~7q$^TDtuk|t?+_~sECLS6`NVgM`-@)?uM;PL&meYh3Eh&v<^7gb30a8)67dqH57F!X(nlHX>aLl=|<@#8Ce+znUgZ*GGnp=vgWc;vL&*gx3X$ z`fb|V{I*@$_F+5Cc8%@c+jF)z%hAYb%K6A$k!z8slh=_)%U_enE3hi;R0vZjQ5aC< zSKO-@uUM`)qa>x|q?D%As6+TuVyph}LB-yf&}4wRW<0lMbDZkq%Dh zna;AVrY=_Zf$mp51wB8#TY6*qvid0f0{tNa2?IBSe1m>Nal<2qd4~OnEeJQnHN@Z! z$sHa$3U`d|+_uwq=k1*{M#@IPMrB4z#=6Ea#xHikcbV-<+V$Rq+vI@BC6iuL2~#iA z5>tYihS^cG7rW_p@7bNU8*eURjx;YeCs=4%#8}krVclc9=hB|Rz1#N&@2#+;v9z$v zu>8DFdLMdUxfNt(Zk1uxV=ZePWL>$Re!unpoc&*HRBWPc8f|%OU2SjKF4`H{rP_5L z*m@xJK&?H8{UQ5e`z4264(Sg42bB)S9DL&_<`inIv+#`-VZAPm_~8 zl3t%tK9hBZl#EP%ouZO*A%$|*^X&Un&D5*s=+B|gb)+HEZl-gkN2L#CSY|xV6wge` zoIme+{#BNGR&F+Pc4&70g}oP^T$H+a?jq@u_oen6qny&q!k159Ub^COrSpK1QuJZxK@NN>c44ov#waHxcHXv zt<(~llF*Xz+lOv{xU=idle-Fc^Go?l&)kFVh2EREf8>70gFO#wA8J3m`$+oHsaRR`Y|FEp=X}potC*`!R8gv%*V?bk-)O#h{8r;_**lGQ zW$!iLKW^4)e)2)*Lq&^zOI0hPwdUilj}2|+ZExE5wYPK}=;*{B!uNN2bdG)U|1{eb z*0tIl_nGc>ff6O%8_)lSWQ_Vf&IbDmGd? zraJa)+;sfIg!9D6WZ>lLRN^$(^yQhYGmpOR`1+P`kT5bEJWH8N{wDaXXkLB3cENg~ zcQIgbbt#D`NW8hMz1+Csurj(DPGTTkSlhN%LEc0DObGy|IH})^ffJR~|L+%^4o)(ul)YMeqpOuk;kxKEO zT^lE=B4Fp>u%9QY{`<|?|HI#mt&y%Jfe*(1FWsd{6eysNsGyWcf!q?cWn2BPDCr6a{QvS?RAG7omA2LtYQP9m^F@)>Otu~a2l;eY1WI(8c;G_+F7z# za)0d+1@gKhJIJ3?;Nl$?^=Qdj`bt}g)cp&x5$4hn!}*e(_-}h$pZ5`rIF?Pr<0;T2 z1+u3=A5-dMaHoXD&W||y2WfBBe)Uf2^mCz%2~yfDS!A}*FXxNROQh#zyUnfJd@nng zd}+RS^I8(gd+nxNe|6VUGRvC**H0&hmXh@0I06N#i@GtIFjHTU9k%pADkMY5{HbxU zcA#<$TgqoAgr9k8A8cV$*!`k|IF`Gn9{g&j-PGzO)4tucvq@_Bv zc*BSQb3`(0rpK3z3adR@_z$byWQoHBt2J(L(gxSx&{eg(mQ$tK9os^IXh?EHC3-9= zs`z>*tW&c6!`%Q@%5)oyS`N!cWr4T@xaOZrwKT6{ZYgk&GWBZ+Exm$12b>+ z9~s(a(QH5eFvhVz0@EOPHRw#q9uW;`fgY!Ae%K1h-mO5|r?P zk4qEYCT)T*Y zJWk$!rN4TG0<~Aq4+XjAMU+t>32z*}2^fePLJw4!nFeAKoZo4^7L#DY>+tR3+IBwD z6a}hr0%r^*`}nJ;6mwTB*>Jns|B6)-BWD=9mQcBuwCG!y2KntrNY18`P1LI=!TGLM zkLZaghxs51MEe2=cQjgz0^N6M22HVaRCvC0Y>3Qsi~?<4BqBg_ex6G{=4v~*L^?Bu zO9jo`>lINAwO8VTuo#iPbl6+TN&kQ`FxJ66WYHeNA*d z5u`Izyb#%7*qlYe<1a0iWlY7E)gMA8G#!vVed5Nn&Yd>_IswnSW^g8%4&Ryy&1B}I zt2_N%xu5BIh0h}EGrD>v9^bPR%u<#zJ%1pRbcIp7bj-adN!yAi?e*m#p5&(={j$~+ zp1008FwcfoD9*+GM==fp}+Co7`E!p3v_q>enmSGV^S+ z4^H7mPF~84(5kiRxe=?k_S*Qd_$LuZx7^7Y;%7gR>1^54+Z81fC2W$r{}sV%vy0+> zOF21LS7))?Z4TdZC=iF$+O_Jj(uhhj*$i|54pZ4AuI|Cn(^WdLHKKg8T4%~izH<5j zq0_Bm!fFcBkk2ov)5bBpz6A~Cm4=_z<`7IDuvdJ(`<2etPcBP7RmEY3xWotw#BgQs zDi6oL{l1D0VPzS6uE#XrlpAg)Zc`#9levItIhU@nZ_i!qA-)-`K3To#wm;S&YIV=U z46~%LB<@NlWBm!|bCFx@V)?!;rNm14^>K}sXSlSZ7tH0`v-mF`A4wEHr_6AO<8_t5 z>Pzk_U~loj7G4Q-R8Jw4$QjNK4GXOiYC{Z3u_8X{j^0iqi`jna4THP$$1+uWtr7}Z?w%KWK^SYr32>{*BZoiC>^hP~SSS>i!%c0O%tLlq&9 zCh65|`B`k~Nf(h^`;O8r_wJR8>6c!$LRrJ)8z01(8_-4Hm~|jCGu-vdws9s}yjS3V zqwQRIq2uUN14i2mAIFT>%q*Z)_N3+TMQ&oopx7xKC&{$yOZhF|9C;*m&zVH;>oUga z#Y=XJ%R5Rv*9sR{Qe^vOlN4{ypyg-sA1DZa*o{nAdm3qP!)Q1dr}tW}Ukpyh=W^M+1T+B8JAQMrZdgj<3S z!$qt260VcYY=E5ylLliC4~~*f;n$*5M_C0ic=_kfO6l+Y$BVsHg zJsv+0Qp^dK^zraH^z~BDuueGgjj$Iu`R~Hv?4Ol~;wxOPA1+ME@NijW2r|CbdR~y>@YU> zAoUsr8d#t}Wt-Oc%n^hfF#axcrxIgu(%Tfg zflgf~^EM_|k68l?XPQ9}ljd;ug2h%Cz`@x#V^XTu+L1w}6-IDQ_m~x_IS==DB~(Qd zAI>7{5W-|h-(H1TmusOIF2D5CUrX7Z*?$?R-41^swHrTjKy3T5hM;uwaMHF^LEMM@ z!BP^>XWT5`$r7JUPVCnet}NWCztYCwf6j{Q?7`+ks`qGxIU^VDzGZjL8u&6Ae}mDy zL`$GvFFLDRG}i5s=aWmm0uyDjyfc}fFI6`eANu3C@LuQdCm7W_vq#A$^9XR7`|-p1 zW_sMP+bJ+l0mDw(S3Xk7q}ZGQm_yv1!;KwSvvZg$y^U*f|3x;veQ=l8WW~zPrKH1jw2^&f~ zP6`?H9>@4$3e7y<91W5W-sI>RyyM{IfV$(CkDrTJSjG=oy)eM^YkbL}FAFO8Y<^2B z-DFYFZrNlcGJ{@Ok&MQJrSdsvuVnTn#uEKAi05x+n&8Wq#)- z{5TP}${6vnHkb1mqU5kE@xV0pj!oAhDrqh(Eq&X8&aKLjGjkvl+@L-tRQ?U~jQ{qt z=CX0y+Q0Ob;m=4B!l%9s?m6Y`v1F|fFWgUF>yVNBEO(!@6+VpV@*1jdY*Z36mshPI z;w;0?>^*ty!CBT}TqKwRJ$T~6J{UcGDq}#X+4gww~4f;Ku2*?a6*m?;rfJ&RNHNGW8ox!P@{*GzoqtP=eM6oTg{9*WpC2$ zmhC2MMJP@VD%D;IF}|PPEBDT#{aQki`~Acw$sp*Q_5^Ij!Hynuac`@Ih?4fiph4#p zF{g*=jnpl$~0TC3rX*QCMkGXx!pxMjwoLwubw3NQoiHFywd)3}0MRqPa?^Fg9$-T%B$9mNS$sQaoj!3xkDV^=}NH9J$cWU0|r05h=hi_-_|ha z7F=}{&DAuVEj>$}Kv1AXCnA{rvs4cxJCw^GKlrh4>Qd58NIi~58p1l*aO4HNA!&?j z%;u7KUeW+~7%fp)|ctPuPZ z1cz&7`yW;vAN99_RYhC%Uq z$TePL75tJ0EhsqCD?*7}y?|NM{_HR&s(@>{B(^YzSlQeBo&=`-Y45C77AesD&0M$r zCVZqXIEw_F9)e*1GM5NStS?<<0TbJT5BPb@A8S|x>Kb-@9XUbf1i#?XTE2D^w_<12 zL_&i6ER|M>)$T~u^u$j2Uei8#f#W)`hGOTIX3`yxu|^dskEq1TD= zwQLp*Zr}@_BbQVpH1$vigRC012Y;z;dwg|ed9`$9-mU++_ndoipK}fA`H*ilSg`jG zRWBP-ARtm_0#Hzq$iC_TIRXcn#c#IHiZ~4xnDAMy*1M9=K0e!w!VE-=CXYy`e|{y^ zk9!)V-;!%9C@Fs2Rd3(`Q?u@3nv*Ga<-!BA)`+rGCsYw$vbT$Ps~j+MhI{@rB+>dU zRgxLOK)Llj!F3Hc(YF%xr1Zlxuy{;UM@$am<}c=6jsQzM$xFE#Yed7^(hNj@LG_#( z(Tw~YT|KW|GO4?)MP{~88FXbVzf;ypff@%&N7YvBmJ9#1Y#8o$=)@8B$u)Hk7(Nep z8ci%KKcB6i;1@xYTX#Ly4mPh4kxJr)IaWa;Pa7A{S4~Al_u=`b+rNM*RrQI2+1#mx z!952Zu#1b6F#-+db0VJNN6(uIEah=D&$<9r(GqHiUvEYM$5C@CiScnM6N5zM*H0%0 z?ZlX?w7k2n49ANYx_3CF^CX;P|D3JYSu%I%=ukt7E?VSj@GhGW`I+CgvGG1X>fWvE+rh)Eb zp}EtSKYQ_P>uU=1M$b75Q2eE%1TD9m4Gcd$VCe2YGqO93*kE+?3 z%2L(o<5mxxd8H$Kq(eiFPn2vjY~^rT!gTf3+C(%jM$Jm!ma8~=G_)!iDYeZ0kkjDx zb=IRF7)FVuVW6c%93h3b8Q|`DHIiwa9ZnoiJG?aYa&|jMg6rl-0sH25vgfM!$-QE# zZREYa`ScXuqKaXXH++73k}oj2BIM-0JdSN^k3X4By&AOa>G~+zI34I-#m2XQC-LeH z?$yP`&o{Fe%V0uvIu-|DtM|;VJe}{6X+=!tI5b_q0Y!~YX|U{BHSqxJEP zyS-vefoR#n0u<}=57!1K|3T*o z)|4Z>`@s(l{dUKKKBguXF(N5cv=*n{w+5SBimH|Z?W#1FV3~SWtc+v2_-4ez7qRec zR|2WaB;}S1o7cWFHYwj4yvGvPs~Q7~{zaK;BSN#t$=WY7g)#+hqULh13Q8l!!#!d{ z1{2{tXI{0-E9mSyG#i!_(ADfLIXCmidEa9&lx>Y|!_8ZGf*)c^H3c)L*K6URip--K zXE9UwRaPRhhOFJvy>nLPO8Ct^L7sT)L(6^&!>tYBFTM)C*1al_b~P}f^yw@@`-S_1 z3%0fR6C=|+k5Gbo!Ec;p<_&pjpQWnQeEC>%CSNyb*mv%X1*xTV8XdC@IDh(2&NDiE z1Dz-xmkMMrJy&x-&X-?QyI`L}e|GeWabFch`5DK}tVf z{fXg+c*e{t>7NVEe3N5UJ>FO-2dX`Yct{3iA8{+K@QP+eX6AwA>o>Zcan?ClFstevfy3SUo*Ebimu=uxvxP!r&EE zMN3iSK((#ZJ=KkRfzGXahF8WWCusrLRs zAJUk3F1n@K;i1ELDDlPza?s?2QfCR7d34rwQG$?7lGN4nCd&|#Ubog1Ozt0+*7<9X_O%d8KJ4s#}xzaW=OG?<9IM zU;r5QV|>en!m5@N&BY1gi3dm1n$C!?4eyjV$6RsMZ*UD)bE)XWZFT1qqXs`c9Oprt z&-gp>#;)n;maht;YVg3%2Y%w=%SqTm9KP>x_GuiSR|N zNUPxn%&z0+P_dPuwxp_PBgz;3))J#tfK>^5Pv;Rc_Eok?KY8ePQ!%G_M>w%=9r;(jn8y z;mg@Q`)f6d$b_$s~H`5Rc7pSik6x*P*3R-cee^F~;*v3VZiq|5C%9kbc}L&o*`Q zEppg+xe~tw1-jSC)1P4s!86haIZdsscN-p?by(z^4L@F{Q$QdY*IlwVPq~ggJ0U%v zuDZir7o~Q!(&0vATOVz-A8oI5gkC+W`ctc-Ant44lROR|!q@5U!Di8DBf|R({@m;1 zVHIafz5Ie_ufLOP_;#(L7+DrvkUTAWl~Etx@&2m&wDxvWM#($TY6QJSx7p7Wh?}HS z{BB_N4#KK+!P)uE=~Bml#YW*<(k@KtLP{0UKH=&4~di`L0d zbD_h!C^31*<&;X%x0if`wA6IFj%YbB<2I!n-u;X}NR zJ;S*d@r+I4_&2V+i2kdW&L^~$8*;SeSCy^SzOlwBV#RtKp)PXw<0Ij+m!SSwUUH|Q zD$(|aN#RS4tM?1LZ|)9QVC*t2-`TRitpw7Ay5oc)2EKOC9TTj-Zt}IagogwUGr+q$+F1z<*QoJI|x(B<%XPifl}9pI~VU01@gp~U=%?k`t8-{_1P#KGg%&y7JeMC41MlPS?A zUIpWt4_qd+<|3_)t{hyDx|)1>DMm+&6&uI+EhS{-kq?a_DjvYf*2?`~%vhjh!otcSZHQg&79PwP@_QBAZeNlUkv0BU zaK!Agz4<4DHhz}erQ;mF->zU5(Zpi|p9Im-Tkfj3RC^wuMiP{>`SvyTTbdT24;x^;?d`lw9%qjikrLkCZyU4IVD6M5lGzufpJm`u;xwn@wc^ literal 0 HcmV?d00001 diff --git a/hutool-core/src/test/resources/test.csv b/hutool-core/src/test/resources/test.csv new file mode 100644 index 000000000..597f1ebbc --- /dev/null +++ b/hutool-core/src/test/resources/test.csv @@ -0,0 +1 @@ +姓名,"性别",关注"对象",年龄 \ No newline at end of file diff --git a/hutool-core/src/test/resources/test.properties b/hutool-core/src/test/resources/test.properties new file mode 100644 index 000000000..242999ade --- /dev/null +++ b/hutool-core/src/test/resources/test.properties @@ -0,0 +1,6 @@ +#-------------------------------------------- +# 配置文件测试 +#-------------------------------------------- + +a = 1 +b = 2 \ No newline at end of file diff --git a/hutool-core/src/test/resources/test.xml b/hutool-core/src/test/resources/test.xml new file mode 100644 index 000000000..d831b2820 --- /dev/null +++ b/hutool-core/src/test/resources/test.xml @@ -0,0 +1,8 @@ + + +Success(成功) +ok +1490 +885 +1 + diff --git a/hutool-cron/pom.xml b/hutool-cron/pom.xml new file mode 100644 index 000000000..99055035c --- /dev/null +++ b/hutool-cron/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + jar + + + cn.hutool + hutool-parent + 4.6.2-SNAPSHOT + + + hutool-cron + ${project.artifactId} + Hutool 定时任务 + + + + cn.hutool + hutool-core + ${project.parent.version} + + + cn.hutool + hutool-setting + ${project.parent.version} + + + diff --git a/hutool-cron/src/main/java/cn/hutool/cron/CronException.java b/hutool-cron/src/main/java/cn/hutool/cron/CronException.java new file mode 100644 index 000000000..b87be0f9d --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/CronException.java @@ -0,0 +1,27 @@ +package cn.hutool.cron; + +import cn.hutool.core.util.StrUtil; + +/** + * 定时任务异常 + * @author xiaoleilu + */ +public class CronException extends RuntimeException{ + private static final long serialVersionUID = 8247610319171014183L; + + public CronException(Throwable e) { + super(e.getMessage(), e); + } + + public CronException(String message) { + super(message); + } + + public CronException(String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params)); + } + + public CronException(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/CronTimer.java b/hutool-cron/src/main/java/cn/hutool/cron/CronTimer.java new file mode 100644 index 000000000..2fb350ceb --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/CronTimer.java @@ -0,0 +1,72 @@ +package cn.hutool.cron; + +import cn.hutool.core.date.DateUnit; +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; + +/** + * 定时任务计时器
+ * 计时器线程每隔一分钟检查一次任务列表,一旦匹配到执行对应的Task + * @author Looly + * + */ +public class CronTimer extends Thread{ + private static final Log log = LogFactory.get(); + + /** 定时单元:秒 */ + private long TIMER_UNIT_SECOND = DateUnit.SECOND.getMillis(); + /** 定时单元:分 */ + private long TIMER_UNIT_MINUTE = DateUnit.MINUTE.getMillis(); + + /** 定时任务是否已经被强制关闭 */ + private boolean isStoped; + private Scheduler scheduler; + + /** + * 构造 + * @param scheduler {@link Scheduler} + */ + public CronTimer(Scheduler scheduler) { + this.scheduler = scheduler; + } + + @Override + public void run() { + final long timerUnit = this.scheduler.matchSecond ? TIMER_UNIT_SECOND : TIMER_UNIT_MINUTE; + + long thisTime = System.currentTimeMillis(); + long nextTime; + long sleep; + while(false == isStoped){ + //下一时间计算是按照上一个执行点开始时间计算的 + nextTime = ((thisTime / timerUnit) + 1) * timerUnit; + sleep = nextTime - System.currentTimeMillis(); + if (sleep > 0 && false == ThreadUtil.safeSleep(sleep)) { + //等待直到下一个时间点,如果被中断直接退出Timer + break; + } + + //执行点,时间记录为执行开始的时间,而非结束时间 + thisTime = System.currentTimeMillis(); + spawnLauncher(thisTime); + } + log.debug("Hutool-cron timer stoped."); + } + + /** + * 关闭定时器 + */ + synchronized public void stopTimer() { + this.isStoped = true; + ThreadUtil.interupt(this, true); + } + + /** + * 启动匹配 + * @param millis 当前时间 + */ + private void spawnLauncher(final long millis){ + this.scheduler.taskLauncherManager.spawnLauncher(millis); + } +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/CronUtil.java b/hutool-cron/src/main/java/cn/hutool/cron/CronUtil.java new file mode 100644 index 000000000..61278a446 --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/CronUtil.java @@ -0,0 +1,205 @@ + +package cn.hutool.cron; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.io.resource.NoResourceException; +import cn.hutool.cron.pattern.CronPattern; +import cn.hutool.cron.task.Task; +import cn.hutool.setting.Setting; +import cn.hutool.setting.SettingRuntimeException; + +/** + * 定时任务工具类
+ * 此工具持有一个全局{@link Scheduler},所有定时任务在同一个调度器中执行
+ * {@link #setMatchSecond(boolean)} 方法用于定义是否使用秒匹配模式,如果为true,则定时任务表达式中的第一位为秒,否则为分,默认是分 + * + * @author xiaoleilu + * + */ +public class CronUtil { + + /** Crontab配置文件 */ + public static final String CRONTAB_CONFIG_PATH = "config/cron.setting"; + public static final String CRONTAB_CONFIG_PATH2 = "cron.setting"; + + private static final Lock lock = new ReentrantLock(); + private static final Scheduler scheduler = new Scheduler(); + private static Setting crontabSetting; + + /** + * 自定义定时任务配置文件 + * + * @param cronSetting 定时任务配置文件 + */ + public static void setCronSetting(Setting cronSetting) { + crontabSetting = cronSetting; + } + + /** + * 自定义定时任务配置文件路径 + * + * @param cronSettingPath 定时任务配置文件路径(相对绝对都可) + */ + public static void setCronSetting(String cronSettingPath) { + try { + crontabSetting = new Setting(cronSettingPath, Setting.DEFAULT_CHARSET, false); + } catch (SettingRuntimeException | NoResourceException e) { + // ignore setting file parse error and no config error + } + } + + /** + * 设置是否支持秒匹配
+ * 此方法用于定义是否使用秒匹配模式,如果为true,则定时任务表达式中的第一位为秒,否则为分,默认是分
+ * + * @param isMatchSecond true支持,false不支持 + */ + public static void setMatchSecond(boolean isMatchSecond) { + scheduler.setMatchSecond(isMatchSecond); + } + + /** + * 加入定时任务 + * + * @param schedulingPattern 定时任务执行时间的crontab表达式 + * @param task 任务 + * @return 定时任务ID + */ + public static String schedule(String schedulingPattern, Task task) { + return scheduler.schedule(schedulingPattern, task); + } + + /** + * 加入定时任务 + * + * @param id 定时任务ID + * @param schedulingPattern 定时任务执行时间的crontab表达式 + * @param task 任务 + * @return 定时任务ID + * @since 3.3.0 + */ + public static String schedule(String id, String schedulingPattern, Task task) { + scheduler.schedule(id, schedulingPattern, task); + return id; + } + + /** + * 加入定时任务 + * + * @param schedulingPattern 定时任务执行时间的crontab表达式 + * @param task 任务 + * @return 定时任务ID + */ + public static String schedule(String schedulingPattern, Runnable task) { + return scheduler.schedule(schedulingPattern, task); + } + + /** + * 批量加入配置文件中的定时任务 + * + * @param cronSetting 定时任务设置文件 + */ + public static void schedule(Setting cronSetting) { + scheduler.schedule(cronSetting); + } + + /** + * 移除任务 + * + * @param schedulerId 任务ID + */ + public static void remove(String schedulerId) { + scheduler.deschedule(schedulerId); + } + + /** + * 移除Task + * + * @param id Task的ID + * @param pattern {@link CronPattern} + * @since 4.0.10 + */ + public static void updatePattern(String id, CronPattern pattern) { + scheduler.updatePattern(id, pattern); + } + + /** + * @return 获得Scheduler对象 + */ + public static Scheduler getScheduler() { + return scheduler; + } + + /** + * 开始,非守护线程模式 + * + * @see #start(boolean) + */ + public static void start() { + start(false); + } + + /** + * 开始 + * + * @param isDeamon 是否以守护线程方式启动,如果为true,则在调用{@link #stop()}方法后执行的定时任务立即结束,否则等待执行完毕才结束。 + */ + synchronized public static void start(boolean isDeamon) { + if (scheduler.isStarted()) { + throw new UtilException("Scheduler has been started, please stop it first!"); + } + + lock.lock(); + try { + if (null == crontabSetting) { + // 尝试查找config/cron.setting + setCronSetting(CRONTAB_CONFIG_PATH); + } + // 尝试查找cron.setting + if (null == crontabSetting) { + setCronSetting(CRONTAB_CONFIG_PATH2); + } + } finally { + lock.unlock(); + } + + schedule(crontabSetting); + scheduler.start(isDeamon); + } + + /** + * 重新启动定时任务
+ * 此方法会清除动态加载的任务,重新启动后,守护线程与否与之前保持一致 + */ + public static void restart() { + lock.lock(); + try { + if (null != crontabSetting) { + //重新读取配置文件 + crontabSetting.load(); + } + if (scheduler.isStarted()) { + //关闭并清除已有任务 + scheduler.stop(true); + } + } finally { + lock.unlock(); + } + + //重新加载任务 + schedule(crontabSetting); + //重新启动 + scheduler.start(); + } + + /** + * 停止 + */ + public static void stop() { + scheduler.stop(); + } + +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/Scheduler.java b/hutool-cron/src/main/java/cn/hutool/cron/Scheduler.java new file mode 100644 index 000000000..2b2eccb70 --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/Scheduler.java @@ -0,0 +1,436 @@ +package cn.hutool.cron; + +import java.util.LinkedHashMap; +import java.util.Map.Entry; +import java.util.TimeZone; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.thread.ExecutorBuilder; +import cn.hutool.core.thread.ThreadFactoryBuilder; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.cron.listener.TaskListener; +import cn.hutool.cron.listener.TaskListenerManager; +import cn.hutool.cron.pattern.CronPattern; +import cn.hutool.cron.task.InvokeTask; +import cn.hutool.cron.task.RunnableTask; +import cn.hutool.cron.task.Task; +import cn.hutool.log.StaticLog; +import cn.hutool.setting.Setting; + +/** + * 任务调度器
+ * + * 调度器启动流程:
+ * + *
+ * 启动Timer =》 启动TaskLauncher =》 启动TaskExecutor
+ * 
+ * + * 调度器关闭流程:
+ * + *
+ * 关闭Timer =》 关闭所有运行中的TaskLauncher =》 关闭所有运行中的TaskExecutor
+ * 
+ * + * 其中: + * + *
+ * TaskLauncher:定时器每分钟调用一次(如果{@link Scheduler#isMatchSecond()}为true每秒调用一次),
+ * 负责检查TaskTable是否有匹配到此时间运行的Task
+ * 
+ * + *
+ * TaskExecutor:TaskLauncher匹配成功后,触发TaskExecutor执行具体的作业,执行完毕销毁
+ * 
+ * + * @author Looly + * + */ +public class Scheduler { + private Lock lock = new ReentrantLock(); + + /** 时区 */ + private TimeZone timezone; + /** 是否已经启动 */ + private boolean started = false; + /** 是否支持秒匹配 */ + protected boolean matchSecond = false; + /** 是否为守护线程 */ + protected boolean daemon; + + /** 定时器 */ + private CronTimer timer; + /** 定时任务表 */ + protected TaskTable taskTable = new TaskTable(this); + /** 启动器管理器 */ + protected TaskLauncherManager taskLauncherManager; + /** 执行器管理器 */ + protected TaskExecutorManager taskExecutorManager; + /** 监听管理器列表 */ + protected TaskListenerManager listenerManager = new TaskListenerManager(); + /** 线程池,用于执行TaskLauncher和TaskExecutor */ + protected ExecutorService threadExecutor; + + // --------------------------------------------------------- Getters and Setters start + /** + * 设置时区 + * + * @param timezone 时区 + * @return this + */ + public Scheduler setTimeZone(TimeZone timezone) { + this.timezone = timezone; + return this; + } + + /** + * 获得时区,默认为 {@link TimeZone#getDefault()} + * + * @return 时区 + */ + public TimeZone getTimeZone() { + return timezone != null ? timezone : TimeZone.getDefault(); + } + + /** + * 设置是否为守护线程
+ * 如果为true,则在调用{@link #stop()}方法后执行的定时任务立即结束,否则等待执行完毕才结束。默认非守护线程 + * + * @param on true为守护线程,否则非守护线程 + * @return this + * @throws CronException 定时任务已经启动抛出此异常 + */ + public Scheduler setDaemon(boolean on) throws CronException { + lock.lock(); + try { + if (this.started) { + throw new CronException("Scheduler already started!"); + } + this.daemon = on; + } finally { + lock.unlock(); + } + return this; + } + + /** + * 是否为守护线程 + * + * @return 是否为守护线程 + */ + public boolean isDeamon() { + return this.daemon; + } + + /** + * 是否支持秒匹配 + * + * @return true使用,false不使用 + */ + public boolean isMatchSecond() { + return this.matchSecond; + } + + /** + * 设置是否支持秒匹配,默认不使用 + * + * @param isMatchSecond true支持,false不支持 + * @return this + */ + public Scheduler setMatchSecond(boolean isMatchSecond) { + this.matchSecond = isMatchSecond; + return this; + } + + /** + * 增加监听器 + * + * @param listener {@link TaskListener} + * @return this + */ + public Scheduler addListener(TaskListener listener) { + this.listenerManager.addListener(listener); + return this; + } + + /** + * 移除监听器 + * + * @param listener {@link TaskListener} + * @return this + */ + public Scheduler removeListener(TaskListener listener) { + this.listenerManager.removeListener(listener); + return this; + } + // --------------------------------------------------------- Getters and Setters end + + // -------------------------------------------------------------------- shcedule start + /** + * 批量加入配置文件中的定时任务
+ * 配置文件格式为: xxx.xxx.xxx.Class.method = * * * * * + * + * @param cronSetting 定时任务设置文件 + * @return this + */ + public Scheduler schedule(Setting cronSetting) { + if (CollectionUtil.isNotEmpty(cronSetting)) { + String group; + for (Entry> groupedEntry : cronSetting.getGroupedMap().entrySet()) { + group = groupedEntry.getKey(); + for (Entry entry : groupedEntry.getValue().entrySet()) { + String jobClass = entry.getKey(); + if (StrUtil.isNotBlank(group)) { + jobClass = group + CharUtil.DOT + jobClass; + } + final String pattern = entry.getValue(); + StaticLog.debug("Load job: {} {}", pattern, jobClass); + try { + schedule(pattern, new InvokeTask(jobClass)); + } catch (Exception e) { + throw new CronException(e, "Schedule [{}] [{}] error!", pattern, jobClass); + } + } + } + } + return this; + } + + /** + * 新增Task,使用随机UUID + * + * @param pattern {@link CronPattern}对应的String表达式 + * @param task {@link Runnable} + * @return ID + */ + public String schedule(String pattern, Runnable task) { + return schedule(pattern, new RunnableTask(task)); + } + + /** + * 新增Task,使用随机UUID + * + * @param pattern {@link CronPattern}对应的String表达式 + * @param task {@link Task} + * @return ID + */ + public String schedule(String pattern, Task task) { + String id = IdUtil.fastUUID(); + schedule(id, pattern, task); + return id; + } + + /** + * 新增Task + * + * @param id ID,为每一个Task定义一个ID + * @param pattern {@link CronPattern}对应的String表达式 + * @param task {@link Runnable} + * @return this + */ + public Scheduler schedule(String id, String pattern, Runnable task) { + return schedule(id, new CronPattern(pattern), new RunnableTask(task)); + } + + /** + * 新增Task + * + * @param id ID,为每一个Task定义一个ID + * @param pattern {@link CronPattern}对应的String表达式 + * @param task {@link Task} + * @return this + */ + public Scheduler schedule(String id, String pattern, Task task) { + return schedule(id, new CronPattern(pattern), task); + } + + /** + * 新增Task + * + * @param id ID,为每一个Task定义一个ID + * @param pattern {@link CronPattern} + * @param task {@link Task} + * @return this + */ + public Scheduler schedule(String id, CronPattern pattern, Task task) { + taskTable.add(id, pattern, task); + return this; + } + + /** + * 移除Task + * + * @param id Task的ID + * @return this + */ + public Scheduler deschedule(String id) { + this.taskTable.remove(id); + return this; + } + + /** + * 更新Task执行的时间规则 + * + * @param id Task的ID + * @param pattern {@link CronPattern} + * @return this + * @since 4.0.10 + */ + public Scheduler updatePattern(String id, CronPattern pattern) { + this.taskTable.updatePattern(id, pattern); + return this; + } + + /** + * 获得指定id的{@link CronPattern} + * + * @param id ID + * @return {@link CronPattern} + * @since 3.1.1 + */ + public CronPattern getPattern(String id) { + return this.taskTable.getPattern(id); + } + + /** + * 获得指定id的{@link Task} + * + * @param id ID + * @return {@link Task} + * @since 3.1.1 + */ + public Task getTask(String id) { + return this.taskTable.getTask(id); + } + + /** + * 是否无任务 + * + * @return true表示无任务 + * @since 4.0.2 + */ + public boolean isEmpty() { + return this.taskTable.isEmpty(); + } + + /** + * 当前任务数 + * + * @return 当前任务数 + * @since 4.0.2 + */ + public int size() { + return this.taskTable.size(); + } + + /** + * 清空任务表 + * @return this + * @since 4.1.17 + */ + public Scheduler clear() { + this.taskTable = new TaskTable(this); + return this; + } + // -------------------------------------------------------------------- shcedule end + + /** + * @return 是否已经启动 + */ + public boolean isStarted() { + return this.started; + } + + /** + * 启动 + * + * @param isDeamon 是否以守护线程方式启动,如果为true,则在调用{@link #stop()}方法后执行的定时任务立即结束,否则等待执行完毕才结束。 + * @return this + */ + public Scheduler start(boolean isDeamon) { + this.daemon = isDeamon; + return start(); + } + + /** + * 启动 + * + * @return this + */ + public Scheduler start() { + lock.lock(); + try { + if (this.started) { + throw new CronException("Schedule is started!"); + } + + // 无界线程池,确保每一个需要执行的线程都可以及时运行,同时复用已有现成避免线程重复创建 + this.threadExecutor = ExecutorBuilder.create().useSynchronousQueue().setThreadFactory(// + ThreadFactoryBuilder.create().setNamePrefix("hutool-cron-").setDaemon(this.daemon).build()// + ).build(); + this.taskLauncherManager = new TaskLauncherManager(this); + this.taskExecutorManager = new TaskExecutorManager(this); + + // Start CronTimer + timer = new CronTimer(this); + timer.setDaemon(this.daemon); + timer.start(); + this.started = true; + } finally { + lock.unlock(); + } + return this; + } + + /** + * 停止定时任务
+ * 此方法调用后会将定时器进程立即结束,如果为守护线程模式,则正在执行的作业也会自动结束,否则作业线程将在执行完成后结束。
+ * 此方法并不会清除任务表中的任务,请调用{@link #clear()} 方法清空任务或者使用{@link #stop(boolean)}方法可选是否清空 + * + * @return this + */ + public Scheduler stop() { + return stop(false); + } + + /** + * 停止定时任务
+ * 此方法调用后会将定时器进程立即结束,如果为守护线程模式,则正在执行的作业也会自动结束,否则作业线程将在执行完成后结束。 + * + * @return this + * @since 4.1.17 + */ + public Scheduler stop(boolean clearTasks) { + lock.lock(); + try { + if (false == started) { + throw new IllegalStateException("Scheduler not started !"); + } + + // 停止CronTimer + this.timer.stopTimer(); + this.timer = null; + + //停止线程池 + this.threadExecutor.shutdown(); + this.threadExecutor = null; + + //可选是否清空任务表 + if(clearTasks) { + clear(); + } + + // 修改标志 + started = false; + } finally { + lock.unlock(); + } + return this; + } + +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/TaskExecutor.java b/hutool-cron/src/main/java/cn/hutool/cron/TaskExecutor.java new file mode 100644 index 000000000..61c6bb2f2 --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/TaskExecutor.java @@ -0,0 +1,41 @@ +package cn.hutool.cron; + +import cn.hutool.cron.task.Task; + +/** + * 作业执行器
+ * 执行具体的作业,执行完毕销毁 + * @author Looly + * + */ +public class TaskExecutor implements Runnable{ + + private Scheduler scheduler; + private Task task; + + /** + * 获得任务对象 + * @return 任务对象 + */ + public Task getTask() { + return task; + } + + public TaskExecutor(Scheduler scheduler, Task task) { + this.scheduler = scheduler; + this.task = task; + } + + @Override + public void run() { + try { + scheduler.listenerManager.notifyTaskStart(this); + task.execute(); + scheduler.listenerManager.notifyTaskSucceeded(this); + } catch (Exception e) { + scheduler.listenerManager.notifyTaskFailed(this, e); + }finally{ + scheduler.taskExecutorManager.notifyExecutorCompleted(this); + } + } +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/TaskExecutorManager.java b/hutool-cron/src/main/java/cn/hutool/cron/TaskExecutorManager.java new file mode 100644 index 000000000..93e2e655a --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/TaskExecutorManager.java @@ -0,0 +1,72 @@ +package cn.hutool.cron; + +import java.util.ArrayList; +import java.util.List; + +import cn.hutool.cron.task.Task; + +/** + * 作业执行管理器
+ * 负责管理作业的启动、停止等 + * + * @author Looly + * @since 3.0.1 + */ +public class TaskExecutorManager { + + protected Scheduler scheduler; + /** 执行器列表 */ + private List executors = new ArrayList<>(); + + public TaskExecutorManager(Scheduler scheduler) { + this.scheduler = scheduler; + } + + /** + * 启动 TaskExecutor + * + * @param task {@link Task} + * @return {@link TaskExecutor} + */ + public TaskExecutor spawnExecutor(Task task) { + final TaskExecutor executor = new TaskExecutor(this.scheduler, task); + synchronized (this.executors) { + this.executors.add(executor); + } + // 子线程是否为deamon线程取决于父线程,因此此处无需显示调用 + // executor.setDaemon(this.scheduler.daemon); +// executor.start(); + this.scheduler.threadExecutor.execute(executor); + return executor; + } + + /** + * 执行器执行完毕调用此方法,将执行器从执行器列表移除 + * + * @param executor 执行器 {@link TaskExecutor} + * @return this + */ + public TaskExecutorManager notifyExecutorCompleted(TaskExecutor executor) { + synchronized (executors) { + executors.remove(executor); + } + return this; + } + + /** + * 停止所有TaskExecutor + * + * @return this + * @deprecated 作业执行器只是执行给定的定时任务线程,无法强制关闭,可通过deamon线程方式关闭之 + */ + @Deprecated + public TaskExecutorManager destroy() { + // synchronized (this.executors) { + // for (TaskExecutor taskExecutor : executors) { + // ThreadUtil.interupt(taskExecutor, false); + // } + // } + this.executors.clear(); + return this; + } +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/TaskLauncher.java b/hutool-cron/src/main/java/cn/hutool/cron/TaskLauncher.java new file mode 100644 index 000000000..63c079e42 --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/TaskLauncher.java @@ -0,0 +1,29 @@ +package cn.hutool.cron; + +/** + * 作业启动器
+ * 负责检查TaskTable是否有匹配到此时运行的Task
+ * 检查完毕后启动器结束 + * + * @author Looly + * + */ +public class TaskLauncher implements Runnable{ + + private Scheduler scheduler; + private long millis; + + public TaskLauncher(Scheduler scheduler, long millis) { + this.scheduler = scheduler; + this.millis = millis; + } + + @Override + public void run() { + //匹配秒部分由用户定义决定,始终不匹配年 + scheduler.taskTable.executeTaskIfMatchInternal(millis); + + //结束通知 + scheduler.taskLauncherManager.notifyLauncherCompleted(this); + } +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/TaskLauncherManager.java b/hutool-cron/src/main/java/cn/hutool/cron/TaskLauncherManager.java new file mode 100644 index 000000000..5510f6acc --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/TaskLauncherManager.java @@ -0,0 +1,64 @@ +package cn.hutool.cron; + +import java.util.ArrayList; +import java.util.List; + +/** + * 作业启动管理器 + * + * @author looly + * + */ +public class TaskLauncherManager { + + protected Scheduler scheduler; + /** 启动器列表 */ + protected List launchers = new ArrayList<>(); + + public TaskLauncherManager(Scheduler scheduler) { + this.scheduler = scheduler; + } + + /** + * 启动 TaskLauncher + * @param millis 触发事件的毫秒数 + * @return {@link TaskLauncher} + */ + protected TaskLauncher spawnLauncher(long millis) { + final TaskLauncher launcher = new TaskLauncher(this.scheduler, millis); + synchronized (this.launchers) { + this.launchers.add(launcher); + } + //子线程是否为deamon线程取决于父线程,因此此处无需显示调用 + //launcher.setDaemon(this.scheduler.daemon); +// launcher.start(); + this.scheduler.threadExecutor.execute(launcher); + return launcher; + } + + /** + * 启动器启动完毕,启动完毕后从执行器列表中移除 + * @param launcher 启动器 {@link TaskLauncher} + */ + protected void notifyLauncherCompleted(TaskLauncher launcher) { + synchronized (launchers) { + launchers.remove(launcher); + } + } + + /** + * 停止所有TaskLauncher + * @return this + * @deprecated 作业启动器只是调用定时任务检查,无法强制关闭,可通过deamon线程方式关闭之 + */ + @Deprecated + public TaskLauncherManager destroy() { + // synchronized (this.launchers) { + // for (TaskLauncher taskLauncher : launchers) { + // ThreadUtil.interupt(taskLauncher, false); + // } + // } + this.launchers.clear(); + return this; + } +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/TaskTable.java b/hutool-cron/src/main/java/cn/hutool/cron/TaskTable.java new file mode 100644 index 000000000..0cf04af09 --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/TaskTable.java @@ -0,0 +1,223 @@ +package cn.hutool.cron; + +import java.util.ArrayList; +import java.util.TimeZone; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import cn.hutool.cron.pattern.CronPattern; +import cn.hutool.cron.task.Task; + +/** + * 定时任务表
+ * 任务表将ID、表达式、任务一一对应,定时任务执行过程中,会周期性检查定时任务表中的所有任务表达式匹配情况,从而执行其对应的任务
+ * 任务的添加、移除使用读写锁保证线程安全性 + * + * @author Looly + * + */ +public class TaskTable { + + private ReadWriteLock lock = new ReentrantReadWriteLock(); + + private Scheduler scheduler; + private TimeZone timezone; + + private ArrayList ids = new ArrayList<>(); + private ArrayList patterns = new ArrayList<>(); + private ArrayList tasks = new ArrayList<>(); + private int size; + + /** + * 构造 + * + * @param scheduler {@link Scheduler} + */ + public TaskTable(Scheduler scheduler) { + this.scheduler = scheduler; + this.timezone = scheduler.getTimeZone(); + } + + /** + * 新增Task + * + * @param id ID + * @param pattern {@link CronPattern} + * @param task {@link Task} + * @return this + */ + public TaskTable add(String id, CronPattern pattern, Task task) { + final Lock writeLock = lock.writeLock(); + try { + writeLock.lock(); + if (ids.contains(id)) { + throw new CronException("Id [{}] has been existed!", id); + } + ids.add(id); + patterns.add(pattern); + tasks.add(task); + size++; + } finally { + writeLock.unlock(); + } + return this; + } + + /** + * 移除Task + * + * @param id Task的ID + */ + public void remove(String id) { + final Lock writeLock = lock.writeLock(); + try { + writeLock.lock(); + final int index = ids.indexOf(id); + if (index > -1) { + tasks.remove(index); + patterns.remove(index); + ids.remove(index); + size--; + } + } finally { + writeLock.unlock(); + } + } + + /** + * 更新某个Task的定时规则 + * + * @param id Task的ID + * @param pattern 新的表达式 + * @return 是否更新成功,如果id对应的规则不存在则不更新 + * @since 4.0.10 + */ + public boolean updatePattern(String id, CronPattern pattern) { + final Lock writeLock = lock.writeLock(); + try { + writeLock.lock(); + final int index = ids.indexOf(id); + if (index > -1) { + patterns.set(index, pattern); + return true; + } + } finally { + writeLock.unlock(); + } + return false; + } + + /** + * 获得指定位置的{@link Task} + * + * @param index 位置 + * @return {@link Task} + * @since 3.1.1 + */ + public Task getTask(int index) { + final Lock readLock = lock.readLock(); + try { + readLock.lock(); + return tasks.get(index); + } finally { + readLock.unlock(); + } + } + + /** + * 获得指定id的{@link Task} + * + * @param id ID + * @return {@link Task} + * @since 3.1.1 + */ + public Task getTask(String id) { + final int index = ids.indexOf(id); + if (index > -1) { + return getTask(index); + } + return null; + } + + /** + * 获得指定位置的{@link CronPattern} + * + * @param index 位置 + * @return {@link CronPattern} + * @since 3.1.1 + */ + public CronPattern getPattern(int index) { + final Lock readLock = lock.readLock(); + try { + readLock.lock(); + return patterns.get(index); + } finally { + readLock.unlock(); + } + } + + /** + * 任务表大小,加入的任务数 + * + * @return 任务表大小,加入的任务数 + * @since 4.0.2 + */ + public int size() { + return this.size; + } + + /** + * 任务表是否为空 + * + * @return true为空 + * @since 4.0.2 + */ + public boolean isEmpty() { + return this.size < 1; + } + + /** + * 获得指定id的{@link CronPattern} + * + * @param id ID + * @return {@link CronPattern} + * @since 3.1.1 + */ + public CronPattern getPattern(String id) { + final int index = ids.indexOf(id); + if (index > -1) { + return getPattern(index); + } + return null; + } + + /** + * 如果时间匹配则执行相应的Task,带读锁 + * + * @param millis 时间毫秒 + */ + public void executeTaskIfMatch(long millis) { + final Lock readLock = lock.readLock(); + try { + readLock.lock(); + executeTaskIfMatchInternal(millis); + } finally { + readLock.unlock(); + } + } + + /** + * 如果时间匹配则执行相应的Task,无锁 + * + * @param millis 时间毫秒 + * @since 3.1.1 + */ + protected void executeTaskIfMatchInternal(long millis) { + for (int i = 0; i < size; i++) { + if (patterns.get(i).match(timezone, millis, this.scheduler.matchSecond)) { + this.scheduler.taskExecutorManager.spawnExecutor(tasks.get(i)); + } + } + } +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/listener/SimpleTaskListener.java b/hutool-cron/src/main/java/cn/hutool/cron/listener/SimpleTaskListener.java new file mode 100644 index 000000000..2e52078db --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/listener/SimpleTaskListener.java @@ -0,0 +1,27 @@ +package cn.hutool.cron.listener; + +import cn.hutool.cron.TaskExecutor; + +/** + * 简单监听实现,不做任何操作
+ * 继承此监听后实现需要的方法即可 + * @author Looly + * + */ +public class SimpleTaskListener implements TaskListener{ + + @Override + public void onStart(TaskExecutor executor) { + } + + @Override + public void onSucceeded(TaskExecutor executor) { + + } + + @Override + public void onFailed(TaskExecutor executor, Throwable exception) { + + } + +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/listener/TaskListener.java b/hutool-cron/src/main/java/cn/hutool/cron/listener/TaskListener.java new file mode 100644 index 000000000..0b8c82aa0 --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/listener/TaskListener.java @@ -0,0 +1,32 @@ +package cn.hutool.cron.listener; + +import cn.hutool.cron.TaskExecutor; + +/** + * 定时任务监听接口
+ * 通过实现此接口,实现对定时任务的各个环节做监听 + * @author Looly + * + */ +public interface TaskListener { + /** + * 定时任务启动时触发 + * @param executor {@link TaskExecutor} + */ + public void onStart(TaskExecutor executor); + + /** + * 任务成功结束时触发 + * + * @param executor {@link TaskExecutor} + */ + public void onSucceeded(TaskExecutor executor); + + /** + * 任务启动失败时触发 + * + * @param executor {@link TaskExecutor} + * @param exception 异常 + */ + public void onFailed(TaskExecutor executor, Throwable exception); +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/listener/TaskListenerManager.java b/hutool-cron/src/main/java/cn/hutool/cron/listener/TaskListenerManager.java new file mode 100644 index 000000000..8e29f8926 --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/listener/TaskListenerManager.java @@ -0,0 +1,88 @@ +package cn.hutool.cron.listener; + +import java.util.ArrayList; +import java.util.List; + +import cn.hutool.cron.TaskExecutor; +import cn.hutool.log.StaticLog; + +/** + * 监听调度器,统一管理监听 + * @author Looly + * + */ +public class TaskListenerManager { + private List listeners = new ArrayList<>(); + + /** + * 增加监听器 + * @param listener {@link TaskListener} + * @return this + */ + public TaskListenerManager addListener(TaskListener listener){ + synchronized (listeners) { + this.listeners.add(listener); + } + return this; + } + + /** + * 移除监听器 + * @param listener {@link TaskListener} + * @return this + */ + public TaskListenerManager removeListener(TaskListener listener){ + synchronized (listeners) { + this.listeners.remove(listener); + } + return this; + } + + /** + * 通知所有监听任务启动器启动 + * @param executor {@link TaskExecutor} + */ + public void notifyTaskStart(TaskExecutor executor) { + synchronized (listeners) { + int size = listeners.size(); + for (int i = 0; i < size; i++) { + TaskListener listenerl = listeners.get(i); + listenerl.onStart(executor); + } + } + } + + /** + * 通知所有监听任务启动器成功结束 + * @param executor {@link TaskExecutor} + */ + public void notifyTaskSucceeded(TaskExecutor executor) { + synchronized (listeners) { + int size = listeners.size(); + for (int i = 0; i < size; i++) { + TaskListener listenerl = listeners.get(i); + listenerl.onSucceeded(executor); + } + } + } + + /** + * 通知所有监听任务启动器结束并失败
+ * 无监听将打印堆栈到命令行 + * @param executor {@link TaskExecutor} + * @param exception 失败原因 + */ + public void notifyTaskFailed(TaskExecutor executor, Throwable exception) { + synchronized (listeners) { + int size = listeners.size(); + if(size > 0){ + for (int i = 0; i < size; i++) { + TaskListener listenerl = listeners.get(i); + listenerl.onFailed(executor, exception); + } + }else{ + StaticLog.error(exception, exception.getMessage()); + } + } + } +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/listener/package-info.java b/hutool-cron/src/main/java/cn/hutool/cron/listener/package-info.java new file mode 100644 index 000000000..b07128c39 --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/listener/package-info.java @@ -0,0 +1,7 @@ +/** + * 定时任务执行监听接口及部分实现 + * + * @author looly + * + */ +package cn.hutool.cron.listener; \ No newline at end of file diff --git a/hutool-cron/src/main/java/cn/hutool/cron/package-info.java b/hutool-cron/src/main/java/cn/hutool/cron/package-info.java new file mode 100644 index 000000000..c96a3f56d --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/package-info.java @@ -0,0 +1,7 @@ +/** + * 定时任务模块,提供类Crontab表达式的定时任务,实现参考了Cron4j,同时可以支持秒级别的定时任务定义和年的定义(同时兼容Crontab、Cron4j、Quartz表达式) + * + * @author looly + * + */ +package cn.hutool.cron; \ No newline at end of file diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/CronPattern.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/CronPattern.java new file mode 100644 index 000000000..30fbbf91d --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/CronPattern.java @@ -0,0 +1,297 @@ +package cn.hutool.cron.pattern; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.TimeZone; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.cron.CronException; +import cn.hutool.cron.pattern.matcher.AlwaysTrueValueMatcher; +import cn.hutool.cron.pattern.matcher.DayOfMonthValueMatcher; +import cn.hutool.cron.pattern.matcher.ValueMatcher; +import cn.hutool.cron.pattern.matcher.ValueMatcherBuilder; +import cn.hutool.cron.pattern.parser.DayOfMonthValueParser; +import cn.hutool.cron.pattern.parser.DayOfWeekValueParser; +import cn.hutool.cron.pattern.parser.HourValueParser; +import cn.hutool.cron.pattern.parser.MinuteValueParser; +import cn.hutool.cron.pattern.parser.MonthValueParser; +import cn.hutool.cron.pattern.parser.SecondValueParser; +import cn.hutool.cron.pattern.parser.ValueParser; +import cn.hutool.cron.pattern.parser.YearValueParser; + +/** + * 定时任务表达式
+ * 表达式类似于Linux的crontab表达式,表达式使用空格分成5个部分,按顺序依次为: + *
    + *
  1. :范围:0~59
  2. + *
  3. :范围:0~23
  4. + *
  5. :范围:1~31,"L"表示月的最后一天
  6. + *
  7. :范围:1~12,同时支持不区分大小写的别名:"jan","feb", "mar", "apr", "may","jun", "jul", "aug", "sep","oct", "nov", "dec"
  8. + *
  9. :范围:0 (Sunday)~6(Saturday),7也可以表示周日,同时支持不区分大小写的别名:"sun","mon", "tue", "wed", "thu","fri", "sat","L"表示周六
  10. + *
+ * + * 为了兼容Quartz表达式,同时支持6位和7位表达式,其中:
+ * + *
+ * 当为6位时,第一位表示,范围0~59,但是第一位不做匹配
+ * 当为7位时,最后一位表示,范围1970~2099,但是第7位不做解析,也不做匹配
+ * 
+ * + * 当定时任务运行到的时间匹配这些表达式后,任务被启动。
+ * 注意: + * + *
+ * 当isMatchSecond为true时才会匹配秒部分
+ * 当isMatchYear为true时才会匹配年部分
+ * 默认都是关闭的
+ * 
+ * + * 对于每一个子表达式,同样支持以下形式: + *
    + *
  • *:表示匹配这个位置所有的时间
  • + *
  • ?:表示匹配这个位置任意的时间(与"*"作用一致)
  • + *
  • */2:表示间隔时间,例如在分上,表示每两分钟,同样*可以使用数字列表代替,逗号分隔
  • + *
  • 2-8:表示连续区间,例如在分上,表示2,3,4,5,6,7,8分
  • + *
  • 2,3,5,8:表示列表
  • + *
  • cronA | cronB:表示多个定时表达式
  • + *
+ * 注意:在每一个子表达式中优先级: + * + *
+ * 间隔(/) > 区间(-) > 列表(,)
+ * 
+ * + * 例如 2,3,6/3中,由于“/”优先级高,因此相当于2,3,(6/3),结果与 2,3,6等价
+ *
+ * + * 一些例子: + *
    + *
  • 5 * * * *:每个点钟的5分执行,00:05,01:05……
  • + *
  • * * * * *:每分钟执行
  • + *
  • */2 * * * *:每两小时执行
  • + *
  • * 12 * * *:12点的每分钟执行
  • + *
  • 59 11 * * 1,2:每周一和周二的11:59执行
  • + *
  • 3-18/5 * * * *:3~18分,每5分钟执行一次,既0:03, 0:08, 0:13, 0:18, 1:03, 1:08……
  • + *
+ * + * @author Looly + * + */ +public class CronPattern { + + private static final ValueParser SECOND_VALUE_PARSER = new SecondValueParser(); + private static final ValueParser MINUTE_VALUE_PARSER = new MinuteValueParser(); + private static final ValueParser HOUR_VALUE_PARSER = new HourValueParser(); + private static final ValueParser DAY_OF_MONTH_VALUE_PARSER = new DayOfMonthValueParser(); + private static final ValueParser MONTH_VALUE_PARSER = new MonthValueParser(); + private static final ValueParser DAY_OF_WEEK_VALUE_PARSER = new DayOfWeekValueParser(); + private static final ValueParser YEAR_VALUE_PARSER = new YearValueParser(); + + private String pattern; + + /** 秒字段匹配列表 */ + private List secondMatchers = new ArrayList<>(); + /** 分字段匹配列表 */ + private List minuteMatchers = new ArrayList<>(); + /** 时字段匹配列表 */ + private List hourMatchers = new ArrayList<>(); + /** 每月几号字段匹配列表 */ + private List dayOfMonthMatchers = new ArrayList<>(); + /** 月字段匹配列表 */ + private List monthMatchers = new ArrayList<>(); + /** 星期字段匹配列表 */ + private List dayOfWeekMatchers = new ArrayList<>(); + /** 年字段匹配列表 */ + private List yearMatchers = new ArrayList<>(); + /** 匹配器个数,取决于复合任务表达式中的单一表达式个数 */ + private int matcherSize; + + /** + * 构造 + * + * @see CronPattern + * + * @param pattern 表达式 + */ + public CronPattern(String pattern) { + this.pattern = pattern; + parseGroupPattern(pattern); + } + + // --------------------------------------------------------------------------------------- match start + /** + * 给定时间是否匹配定时任务表达式 + * + * @param millis 时间毫秒数 + * @param isMatchSecond 是否匹配秒 + * @return 如果匹配返回 true, 否则返回 false + */ + public boolean match(long millis, boolean isMatchSecond) { + return match(TimeZone.getDefault(), millis, isMatchSecond); + } + + /** + * 给定时间是否匹配定时任务表达式 + * + * @param timezone 时区 {@link TimeZone} + * @param millis 时间毫秒数 + * @param isMatchSecond 是否匹配秒 + * @return 如果匹配返回 true, 否则返回 false + */ + public boolean match(TimeZone timezone, long millis, boolean isMatchSecond) { + final GregorianCalendar calendar = new GregorianCalendar(timezone); + calendar.setTimeInMillis(millis); + return match(calendar, isMatchSecond); + } + + /** + * 给定时间是否匹配定时任务表达式 + * + * @param calendar 时间 + * @param isMatchSecond 是否匹配秒 + * @return 如果匹配返回 true, 否则返回 false + */ + public boolean match(GregorianCalendar calendar, boolean isMatchSecond) { + final int second = calendar.get(Calendar.SECOND); + final int minute = calendar.get(Calendar.MINUTE); + final int hour = calendar.get(Calendar.HOUR_OF_DAY); + final int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH); + final int month = calendar.get(Calendar.MONTH) + 1;// 月份从1开始 + final int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK) - 1; // 星期从0开始,0和7都表示周日 + final int year = calendar.get(Calendar.YEAR); + + boolean eval; + for (int i = 0; i < matcherSize; i++) { + eval = (isMatchSecond ? secondMatchers.get(i).match(second) : true) // 匹配秒(非秒匹配模式下始终返回true) + && minuteMatchers.get(i).match(minute)// 匹配分 + && hourMatchers.get(i).match(hour)// 匹配时 + && isMatchDayOfMonth(dayOfMonthMatchers.get(i), dayOfMonth, month, calendar.isLeapYear(year))// 匹配日 + && monthMatchers.get(i).match(month) // 匹配月 + && dayOfWeekMatchers.get(i).match(dayOfWeek)// 匹配周 + && isMatch(yearMatchers, i, year);// 匹配年 + if (eval) { + return true; + } + } + return false; + } + // --------------------------------------------------------------------------------------- match end + + @Override + public String toString() { + return this.pattern; + } + + // -------------------------------------------------------------------------------------- Private method start + /** + * 是否匹配日(指定月份的第几天) + * + * @param matcher {@link ValueMatcher} + * @param dayOfMonth 日 + * @param month 月 + * @param isLeapYear 是否闰年 + * @return 是否匹配 + */ + private static boolean isMatchDayOfMonth(ValueMatcher matcher, int dayOfMonth, int month, boolean isLeapYear) { + return ((matcher instanceof DayOfMonthValueMatcher) // + ? ((DayOfMonthValueMatcher) matcher).match(dayOfMonth, month, isLeapYear) // + : matcher.match(dayOfMonth)); + } + + /** + * 是否匹配指定的日期时间位置 + * + * @param matchers 匹配器列表 + * @param index 位置 + * @param value 被匹配的值 + * @return 是否匹配 + * @since 4.0.2 + */ + private static boolean isMatch(List matchers, int index, int value) { + return (matchers.size() > index) ? matchers.get(index).match(value) : true; + } + + /** + * 解析复合任务表达式 + * + * @param groupPattern 复合表达式 + */ + private void parseGroupPattern(String groupPattern) { + List patternList = StrUtil.split(groupPattern, '|'); + for (String pattern : patternList) { + parseSinglePattern(pattern); + } + } + + /** + * 解析单一定时任务表达式 + * + * @param pattern 表达式 + */ + private void parseSinglePattern(String pattern) { + final String[] parts = pattern.split("\\s"); + + int offset = 0;// 偏移量用于兼容Quartz表达式,当表达式有6或7项时,第一项为秒 + if (parts.length == 6 || parts.length == 7) { + offset = 1; + } else if (parts.length != 5) { + throw new CronException("Pattern [{}] is invalid, it must be 5-7 parts!", pattern); + } + + // 秒 + if (1 == offset) {// 支持秒的表达式 + try { + this.secondMatchers.add(ValueMatcherBuilder.build(parts[0], SECOND_VALUE_PARSER)); + } catch (Exception e) { + throw new CronException(e, "Invalid pattern [{}], parsing 'second' field error!", pattern); + } + } else {// 不支持秒的表达式,则第一位按照表达式生成时间的秒数赋值,表示整分匹配 + this.secondMatchers.add(ValueMatcherBuilder.build(String.valueOf(DateUtil.date().second()), SECOND_VALUE_PARSER)); + } + // 分 + try { + this.minuteMatchers.add(ValueMatcherBuilder.build(parts[0 + offset], MINUTE_VALUE_PARSER)); + } catch (Exception e) { + throw new CronException(e, "Invalid pattern [{}], parsing 'minute' field error!", pattern); + } + // 小时 + try { + this.hourMatchers.add(ValueMatcherBuilder.build(parts[1 + offset], HOUR_VALUE_PARSER)); + } catch (Exception e) { + throw new CronException(e, "Invalid pattern [{}], parsing 'hour' field error!", pattern); + } + // 每月第几天 + try { + this.dayOfMonthMatchers.add(ValueMatcherBuilder.build(parts[2 + offset], DAY_OF_MONTH_VALUE_PARSER)); + } catch (Exception e) { + throw new CronException(e, "Invalid pattern [{}], parsing 'day of month' field error!", pattern); + } + // 月 + try { + this.monthMatchers.add(ValueMatcherBuilder.build(parts[3 + offset], MONTH_VALUE_PARSER)); + } catch (Exception e) { + throw new CronException(e, "Invalid pattern [{}], parsing 'month' field error!", pattern); + } + // 星期几 + try { + this.dayOfWeekMatchers.add(ValueMatcherBuilder.build(parts[4 + offset], DAY_OF_WEEK_VALUE_PARSER)); + } catch (Exception e) { + throw new CronException(e, "Invalid pattern [{}], parsing 'day of week' field error!", pattern); + } + // 年 + if (parts.length == 7) {// 支持年的表达式 + try { + this.yearMatchers.add(ValueMatcherBuilder.build(parts[6], YEAR_VALUE_PARSER)); + } catch (Exception e) { + throw new CronException(e, "Invalid pattern [{}], parsing 'year' field error!", pattern); + } + } else {// 不支持年的表达式,全部匹配 + this.secondMatchers.add(new AlwaysTrueValueMatcher()); + } + matcherSize++; + } + // -------------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/CronPatternUtil.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/CronPatternUtil.java new file mode 100644 index 000000000..3f7113873 --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/CronPatternUtil.java @@ -0,0 +1,103 @@ +package cn.hutool.cron.pattern; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUnit; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.lang.Assert; + +/** + * 定时任务表达式工具类 + * + * @author looly + * + */ +public class CronPatternUtil { + + /** + * 列举指定日期之后(到开始日期对应年年底)内第一个匹配表达式的日期 + * + * @param pattern 表达式 + * @param start 起始时间 + * @param isMatchSecond 是否匹配秒 + * @return 日期 + * @since 4.5.8 + */ + public static Date nextDateAfter(CronPattern pattern, Date start, boolean isMatchSecond) { + List matchedDates = matchedDates(pattern, start.getTime(), DateUtil.endOfYear(start).getTime(), 1, isMatchSecond); + if (CollUtil.isNotEmpty(matchedDates)) { + return matchedDates.get(0); + } + return null; + } + + /** + * 列举指定日期之后(到开始日期对应年年底)内所有匹配表达式的日期 + * + * @param patternStr 表达式字符串 + * @param start 起始时间 + * @param count 列举数量 + * @param isMatchSecond 是否匹配秒 + * @return 日期列表 + */ + public static List matchedDates(String patternStr, Date start, int count, boolean isMatchSecond) { + return matchedDates(patternStr, start, DateUtil.endOfYear(start), count, isMatchSecond); + } + + /** + * 列举指定日期范围内所有匹配表达式的日期 + * + * @param patternStr 表达式字符串 + * @param start 起始时间 + * @param end 结束时间 + * @param count 列举数量 + * @param isMatchSecond 是否匹配秒 + * @return 日期列表 + */ + public static List matchedDates(String patternStr, Date start, Date end, int count, boolean isMatchSecond) { + return matchedDates(patternStr, start.getTime(), end.getTime(), count, isMatchSecond); + } + + /** + * 列举指定日期范围内所有匹配表达式的日期 + * + * @param patternStr 表达式字符串 + * @param start 起始时间 + * @param end 结束时间 + * @param count 列举数量 + * @param isMatchSecond 是否匹配秒 + * @return 日期列表 + */ + public static List matchedDates(String patternStr, long start, long end, int count, boolean isMatchSecond) { + return matchedDates(new CronPattern(patternStr), start, end, count, isMatchSecond); + } + + /** + * 列举指定日期范围内所有匹配表达式的日期 + * + * @param pattern 表达式 + * @param start 起始时间 + * @param end 结束时间 + * @param count 列举数量 + * @param isMatchSecond 是否匹配秒 + * @return 日期列表 + */ + public static List matchedDates(CronPattern pattern, long start, long end, int count, boolean isMatchSecond) { + Assert.isTrue(start < end, "Start date is later than end !"); + + final List result = new ArrayList<>(count); + long step = isMatchSecond ? DateUnit.SECOND.getMillis() : DateUnit.MINUTE.getMillis(); + for (long i = start; i < end; i += step) { + if (pattern.match(i, isMatchSecond)) { + result.add(DateUtil.date(i)); + if (result.size() >= count) { + break; + } + } + } + return result; + } +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/AlwaysTrueValueMatcher.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/AlwaysTrueValueMatcher.java new file mode 100644 index 000000000..c7c30ce7e --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/AlwaysTrueValueMatcher.java @@ -0,0 +1,21 @@ +package cn.hutool.cron.pattern.matcher; + +import cn.hutool.core.util.StrUtil; + +/** + * 值匹配,始终返回true + * @author Looly + * + */ +public class AlwaysTrueValueMatcher implements ValueMatcher{ + + @Override + public boolean match(Integer t) { + return true; + } + + @Override + public String toString() { + return StrUtil.format("[Matcher]: always true."); + } +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/BoolArrayValueMatcher.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/BoolArrayValueMatcher.java new file mode 100644 index 000000000..949e2e811 --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/BoolArrayValueMatcher.java @@ -0,0 +1,36 @@ +package cn.hutool.cron.pattern.matcher; + +import java.util.Collections; +import java.util.List; + +import cn.hutool.core.util.StrUtil; + +/** + * 将表达式中的数字值列表转换为Boolean数组,匹配时匹配相应数组位 + * @author Looly + * + */ +public class BoolArrayValueMatcher implements ValueMatcher{ + + boolean[] bValues; + + public BoolArrayValueMatcher(List intValueList) { + bValues = new boolean[Collections.max(intValueList) + 1]; + for (Integer value : intValueList) { + bValues[value] = true; + } + } + + @Override + public boolean match(Integer value) { + if(null == value || value >= bValues.length){ + return false; + } + return bValues[value]; + } + + @Override + public String toString() { + return StrUtil.format("Matcher:{}", (Object)this.bValues); + } +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/DayOfMonthValueMatcher.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/DayOfMonthValueMatcher.java new file mode 100644 index 000000000..fd859a81f --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/DayOfMonthValueMatcher.java @@ -0,0 +1,58 @@ +package cn.hutool.cron.pattern.matcher; + +import java.util.List; + +/** + * 每月第几天匹配
+ * 考虑每月的天数不同,切存在闰年情况,日匹配单独使用 + * + * @author Looly + * + */ +public class DayOfMonthValueMatcher extends BoolArrayValueMatcher { + + private static final int[] LAST_DAYS = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; + + /** + * 构造 + * + * @param intValueList 匹配的日值 + */ + public DayOfMonthValueMatcher(List intValueList) { + super(intValueList); + } + + /** + * 给定的日期是否匹配当前匹配器 + * + * @param value 被检查的值,此处为日 + * @param month 实际的月份 + * @param isLeapYear 是否闰年 + * @return 是否匹配 + */ + public boolean match(int value, int month, boolean isLeapYear) { + return (super.match(value) // 在约定日范围内的某一天 + //匹配器中用户定义了最后一天(32表示最后一天) + || (value > 27 && match(32) && isLastDayOfMonth(value, month, isLeapYear))); + } + + /** + * 是否为本月最后一天,规则如下: + *
+	 * 1、闰年2月匹配是否为29
+	 * 2、其它月份是否匹配最后一天的日期(可能为30或者31)
+	 * 
+ * + * @param value 被检查的值 + * @param month 月份 + * @param isLeapYear 是否闰年 + * @return 是否为本月最后一天 + */ + private static boolean isLastDayOfMonth(int value, int month, boolean isLeapYear) { + if (isLeapYear && month == 2) { + return value == 29; + } else { + return value == LAST_DAYS[month - 1]; + } + } +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/ValueMatcher.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/ValueMatcher.java new file mode 100644 index 000000000..0fa9546c4 --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/ValueMatcher.java @@ -0,0 +1,13 @@ +package cn.hutool.cron.pattern.matcher; + +import cn.hutool.core.lang.Matcher; + +/** + * 值匹配器
+ * 用于匹配日期位中对应数字是否匹配 + * @author Looly + * + */ +public interface ValueMatcher extends Matcher{ + +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/ValueMatcherBuilder.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/ValueMatcherBuilder.java new file mode 100644 index 000000000..6b2b616ec --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/ValueMatcherBuilder.java @@ -0,0 +1,196 @@ +package cn.hutool.cron.pattern.matcher; + +import java.util.ArrayList; +import java.util.List; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.cron.CronException; +import cn.hutool.cron.pattern.parser.DayOfMonthValueParser; +import cn.hutool.cron.pattern.parser.ValueParser; +import cn.hutool.cron.pattern.parser.YearValueParser; + +/** + * {@link ValueMatcher} 构建器,用于构建表达式中每一项的匹配器 + * @author Looly + * + */ +public class ValueMatcherBuilder { + + /** + * 处理定时任务表达式每个时间字段
+ * 多个时间使用逗号分隔 + * + * @param value 某个时间字段 + * @param parser 针对这个时间字段的解析器 + * @return List + */ + public static ValueMatcher build(String value, ValueParser parser) { + if (isMatchAllStr(value)) { + //兼容Quartz的"?"表达式,不会出现互斥情况,与"*"作用相同 + return new AlwaysTrueValueMatcher(); + } + + List values = parseArray(value, parser); + if (values.size() == 0) { + throw new CronException("Invalid field: [{}]", value); + } + + if (parser instanceof DayOfMonthValueParser) { + //考虑每月的天数不同,且存在闰年情况,日匹配单独使用 + return new DayOfMonthValueMatcher(values); + }else if(parser instanceof YearValueParser){ + //考虑年数字太大,不适合boolean数组,单独使用列表遍历匹配 + return new YearValueMatcher(values); + }else { + return new BoolArrayValueMatcher(values); + } + } + + /** + * 处理数组形式表达式
+ * 处理的形式包括: + *
    + *
  • a*
  • + *
  • a,b,c,d
  • + *
+ * @param value 子表达式值 + * @param parser 针对这个字段的解析器 + * @return 值列表 + */ + private static List parseArray(String value, ValueParser parser){ + final List values = new ArrayList<>(); + + final List parts = StrUtil.split(value, StrUtil.C_COMMA); + for (String part : parts) { + CollectionUtil.addAllIfNotContains(values, parseStep(part, parser)); + } + return values; + } + + /** + * 处理间隔形式的表达式
+ * 处理的形式包括: + *
    + *
  • a*
  • + *
  • a/b*/b
  • + *
  • a-b/2
  • + *
+ * + * @param value 表达式值 + * @param parser 针对这个时间字段的解析器 + * @return List + */ + private static List parseStep(String value, ValueParser parser) { + final List parts = StrUtil.split(value, StrUtil.C_SLASH); + int size = parts.size(); + + List results; + if (size == 1) {// 普通形式 + results = parseRange(value, -1, parser); + } else if (size == 2) {// 间隔形式 + final int step = parser.parse(parts.get(1)); + if (step < 1) { + throw new CronException("Non positive divisor for field: [{}]", value); + } + results = parseRange(parts.get(0), step, parser); + } else { + throw new CronException("Invalid syntax of field: [{}]", value); + } + return results; + } + + /** + * 处理表达式中范围表达式 处理的形式包括: + *
    + *
  • *
  • + *
  • 2
  • + *
  • 3-8
  • + *
  • 8-3
  • + *
  • 3-3
  • + *
+ * + * @param value 范围表达式 + * @param step 步进 + * @param parser 针对这个时间字段的解析器 + * @return List + */ + private static List parseRange(String value, int step, ValueParser parser) { + final List results = new ArrayList<>(); + + // 全部匹配形式 + if (value.length() <= 2) { + //根据步进的第一个数字确定起始时间,类似于 12/3则从12(秒、分等)开始 + int minValue = parser.getMin(); + if(false == isMatchAllStr(value)) { + minValue = Math.max(minValue, parser.parse(value)); + }else { + //在全匹配模式下,如果步进不存在,表示步进为1 + if(step < 1) { + step = 1; + } + } + if(step > 0) { + final int maxValue = parser.getMax(); + if(minValue > maxValue) { + throw new CronException("Invalid value {} > {}", minValue, maxValue); + } + //有步进 + for (int i = minValue; i <= maxValue; i+=step) { + results.add(i); + } + } else { + //固定时间 + results.add(minValue); + } + return results; + } + + //Range模式 + List parts = StrUtil.split(value, '-'); + int size = parts.size(); + if (size == 1) {// 普通值 + final int v1 = parser.parse(value); + if(step > 0) {//类似 20/2的形式 + NumberUtil.appendRange(v1, parser.getMax(), step, results); + }else { + results.add(v1); + } + } else if (size == 2) {// range值 + final int v1 = parser.parse(parts.get(0)); + final int v2 = parser.parse(parts.get(1)); + if(step < 1) { + //在range模式下,如果步进不存在,表示步进为1 + step = 1; + } + if (v1 < v2) {// 正常范围,例如:2-5 + NumberUtil.appendRange(v1, v2, step, results); + } else if (v1 > v2) {// 逆向范围,反选模式,例如:5-2 + NumberUtil.appendRange(v1, parser.getMax(), step, results); + NumberUtil.appendRange(parser.getMin(), v2, step, results); + } else {// v1 == v2,此时与单值模式一致 + if(step > 0) {//类似 20/2的形式 + NumberUtil.appendRange(v1, parser.getMax(), step, results); + }else { + results.add(v1); + } + } + } else { + throw new CronException("Invalid syntax of field: [{}]", value); + } + return results; + } + + /** + * 是否为全匹配符
+ * 全匹配符指 * 或者 ? + * + * @param value 被检查的值 + * @return 是否为全匹配符 + * @since 4.1.18 + */ + private static boolean isMatchAllStr(String value) { + return (1 == value.length()) && ("*".equals(value) || "?".equals(value)); + } +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/YearValueMatcher.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/YearValueMatcher.java new file mode 100644 index 000000000..a22676b88 --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/YearValueMatcher.java @@ -0,0 +1,23 @@ +package cn.hutool.cron.pattern.matcher; + +import java.util.List; + +/** + * 年匹配
+ * 考虑年数字太大,不适合boolean数组,单独使用列表遍历匹配 + * @author Looly + * + */ +public class YearValueMatcher implements ValueMatcher{ + + private List valueList; + + public YearValueMatcher(List intValueList) { + this.valueList = intValueList; + } + + @Override + public boolean match(Integer t) { + return valueList.contains(t); + } +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/package-info.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/package-info.java new file mode 100644 index 000000000..4288a1029 --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/package-info.java @@ -0,0 +1,7 @@ +/** + * 定时任务表达式匹配器,内部使用 + * + * @author looly + * + */ +package cn.hutool.cron.pattern.matcher; \ No newline at end of file diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/package-info.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/package-info.java new file mode 100644 index 000000000..e83b6e745 --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/package-info.java @@ -0,0 +1,7 @@ +/** + * 定时任务表达式解析,核心为CronPattern + * + * @author looly + * + */ +package cn.hutool.cron.pattern; \ No newline at end of file diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/DayOfMonthValueParser.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/DayOfMonthValueParser.java new file mode 100644 index 000000000..29edcf667 --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/DayOfMonthValueParser.java @@ -0,0 +1,26 @@ +package cn.hutool.cron.pattern.parser; + +import cn.hutool.cron.CronException; + +/** + * 每月的几号值处理
+ * 每月最多31天,32和“L”都表示最后一天 + * + * @author Looly + * + */ +public class DayOfMonthValueParser extends SimpleValueParser { + + public DayOfMonthValueParser() { + super(1, 31); + } + + @Override + public int parse(String value) throws CronException { + if (value.equalsIgnoreCase("L") || value.equals("32")) {// 每月最后一天 + return 32; + } else { + return super.parse(value); + } + } +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/DayOfWeekValueParser.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/DayOfWeekValueParser.java new file mode 100644 index 000000000..6b2347865 --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/DayOfWeekValueParser.java @@ -0,0 +1,53 @@ +package cn.hutool.cron.pattern.parser; + +import cn.hutool.cron.CronException; + +/** + * 星期值处理
+ * 1表示星期一,2表示星期二,依次类推,0和7都可以表示星期日 + * + * @author Looly + * + */ +public class DayOfWeekValueParser extends SimpleValueParser { + + /** Weeks aliases. */ + private static final String[] ALIASES = { "sun", "mon", "tue", "wed", "thu", "fri", "sat" }; + + public DayOfWeekValueParser() { + super(0, 7); + } + + /** + * 对于星期提供转换
+ * 1表示星期一,2表示星期二,依次类推,0和7都可以表示星期日 + */ + @Override + public int parse(String value) throws CronException { + try { + return super.parse(value) % 7; + } catch (Exception e) { + return parseAlias(value); + } + } + + /** + * 解析别名 + * @param value 别名值 + * @return 月份int值 + * @throws CronException + */ + private int parseAlias(String value) throws CronException { + if(value.equalsIgnoreCase("L")){ + //最后一天为星期六 + return ALIASES.length - 1; + } + + for (int i = 0; i < ALIASES.length; i++) { + if (ALIASES[i].equalsIgnoreCase(value)) { + return i; + } + } + throw new CronException("Invalid month alias: {}", value); + } +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/HourValueParser.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/HourValueParser.java new file mode 100644 index 000000000..015be8f2c --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/HourValueParser.java @@ -0,0 +1,14 @@ +package cn.hutool.cron.pattern.parser; + +/** + * 小时值处理 + * @author Looly + * + */ +public class HourValueParser extends SimpleValueParser{ + + public HourValueParser() { + super(0, 23); + } + +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/MinuteValueParser.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/MinuteValueParser.java new file mode 100644 index 000000000..6a28c2e62 --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/MinuteValueParser.java @@ -0,0 +1,14 @@ +package cn.hutool.cron.pattern.parser; + +/** + * 分钟值处理 + * @author Looly + * + */ +public class MinuteValueParser extends SimpleValueParser{ + + public MinuteValueParser() { + super(0, 59); + } + +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/MonthValueParser.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/MonthValueParser.java new file mode 100644 index 000000000..6480cc8c3 --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/MonthValueParser.java @@ -0,0 +1,43 @@ +package cn.hutool.cron.pattern.parser; + +import cn.hutool.cron.CronException; + +/** + * 月份值处理 + * + * @author Looly + * + */ +public class MonthValueParser extends SimpleValueParser { + + /** Months aliases. */ + private static final String[] ALIASES = { "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec" }; + + public MonthValueParser() { + super(1, 12); + } + + @Override + public int parse(String value) throws CronException { + try { + return super.parse(value); + } catch (Exception e) { + return parseAlias(value); + } + } + + /** + * 解析别名 + * @param value 别名值 + * @return 月份int值 + * @throws CronException + */ + private int parseAlias(String value) throws CronException { + for (int i = 0; i < ALIASES.length; i++) { + if (ALIASES[i].equalsIgnoreCase(value)) { + return i + 1; + } + } + throw new CronException("Invalid month alias: {}", value); + } +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/SecondValueParser.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/SecondValueParser.java new file mode 100644 index 000000000..abd54a66a --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/SecondValueParser.java @@ -0,0 +1,9 @@ +package cn.hutool.cron.pattern.parser; + +/** + * 秒值处理 + * @author Looly + * + */ +public class SecondValueParser extends MinuteValueParser{ +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/SimpleValueParser.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/SimpleValueParser.java new file mode 100644 index 000000000..cd73642f5 --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/SimpleValueParser.java @@ -0,0 +1,50 @@ +package cn.hutool.cron.pattern.parser; + +import cn.hutool.cron.CronException; + +/** + * 简易值转换器。将给定String值转为int + * @author Looly + * + */ +public class SimpleValueParser implements ValueParser { + + /** 最小值(包括) */ + protected int min; + /** 最大值(包括) */ + protected int max; + + public SimpleValueParser(int min, int max) { + if(min > max){ + this.min = max; + this.max = min; + }else{ + this.min = min; + this.max = max; + } + } + + @Override + public int parse(String value) throws CronException { + int i; + try { + i = Integer.parseInt(value); + } catch (NumberFormatException e) { + throw new CronException(e, "Invalid integer value: '{}'", value); + } + if (i < min || i > max) { + throw new CronException("Value {} out of range: [{} , {}]", i, min, max); + } + return i; + } + + @Override + public int getMin() { + return this.min; + } + + @Override + public int getMax() { + return this.max; + } +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/ValueParser.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/ValueParser.java new file mode 100644 index 000000000..4096fee34 --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/ValueParser.java @@ -0,0 +1,37 @@ +package cn.hutool.cron.pattern.parser; + +/** + * 值处理接口
+ * 值处理用于限定表达式中相应位置的值范围,并转换表达式值为int值 + * + * @author Looly + */ +public interface ValueParser { + + /** + * 处理String值并转为int
+ * 转换包括: + *
    + *
  1. 数字字符串转为数字
  2. + *
  3. 别名转为对应的数字(如月份和星期)
  4. + *
+ * + * @param value String值 + * @return int + */ + public int parse(String value); + + /** + * 返回最小值 + * + * @return 最小值 + */ + public int getMin(); + + /** + * 返回最大值 + * + * @return 最大值 + */ + public int getMax(); +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/YearValueParser.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/YearValueParser.java new file mode 100644 index 000000000..a6ef2fdc4 --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/YearValueParser.java @@ -0,0 +1,14 @@ +package cn.hutool.cron.pattern.parser; + +/** + * 年值处理 + * @author Looly + * + */ +public class YearValueParser extends SimpleValueParser{ + + public YearValueParser() { + super(1970, 2099); + } + +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/package-info.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/package-info.java new file mode 100644 index 000000000..fff0b12f7 --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/package-info.java @@ -0,0 +1,7 @@ +/** + * 定时任务表达式解析器,内部使用 + * + * @author looly + * + */ +package cn.hutool.cron.pattern.parser; \ No newline at end of file diff --git a/hutool-cron/src/main/java/cn/hutool/cron/task/InvokeTask.java b/hutool-cron/src/main/java/cn/hutool/cron/task/InvokeTask.java new file mode 100644 index 000000000..b283efc03 --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/task/InvokeTask.java @@ -0,0 +1,69 @@ +package cn.hutool.cron.task; + +import java.lang.reflect.Method; + +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.util.ClassLoaderUtil; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.cron.CronException; + +/** + * 反射执行任务
+ * 通过传入类名#方法名,通过反射执行相应的方法
+ * 如果是静态方法直接执行,如果是对象方法,需要类有默认的构造方法。 + * + * @author Looly + * + */ +public class InvokeTask implements Task{ + + private Class clazz; + private Object obj; + private Method method; + + /** + * 构造 + * @param classNameWithMethodName 类名与方法名的字符串表示,方法名和类名使用#隔开或者.隔开 + */ + public InvokeTask(String classNameWithMethodName) { + 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); + if(StrUtil.isBlank(className)) { + throw new IllegalArgumentException("Class name is blank !"); + } + this.clazz = ClassLoaderUtil.loadClass(className); + if(null == this.clazz) { + throw new IllegalArgumentException("Load class with name of [" + className + "] fail !"); + } + this.obj = ReflectUtil.newInstanceIfPossible(this.clazz); + + //方法 + final String methodName = classNameWithMethodName.substring(splitIndex + 1); + if(StrUtil.isBlank(methodName)) { + throw new IllegalArgumentException("Method name is blank !"); + } + this.method = ClassUtil.getPublicMethod(this.clazz, methodName); + if(null == this.method) { + throw new IllegalArgumentException("No method with name of [" + methodName + "] !"); + } + } + + @Override + public void execute() { + try { + ReflectUtil.invoke(this.obj, this.method, new Object[]{}); + } catch (UtilException e) { + throw new CronException(e.getCause()); + } + } +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/task/RunnableTask.java b/hutool-cron/src/main/java/cn/hutool/cron/task/RunnableTask.java new file mode 100644 index 000000000..0f4fbe046 --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/task/RunnableTask.java @@ -0,0 +1,19 @@ +package cn.hutool.cron.task; + +/** + * {@link Runnable} 的 {@link Task}包装 + * @author Looly + * + */ +public class RunnableTask implements Task{ + private Runnable runnable; + + public RunnableTask(Runnable runnable) { + this.runnable = runnable; + } + + @Override + public void execute() { + runnable.run(); + } +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/task/Task.java b/hutool-cron/src/main/java/cn/hutool/cron/task/Task.java new file mode 100644 index 000000000..b9ce3e16c --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/task/Task.java @@ -0,0 +1,14 @@ +package cn.hutool.cron.task; + +/** + * 定时作业接口,通过实现execute方法执行具体的任务
+ * @author Looly + * + */ +public interface Task { + + /** + * 执行作业 + */ + public void execute(); +} diff --git a/hutool-cron/src/main/java/cn/hutool/cron/task/package-info.java b/hutool-cron/src/main/java/cn/hutool/cron/task/package-info.java new file mode 100644 index 000000000..9b3ec08d0 --- /dev/null +++ b/hutool-cron/src/main/java/cn/hutool/cron/task/package-info.java @@ -0,0 +1,7 @@ +/** + * 定时任务中作业的抽象封装和实现,包括Runnable实现和反射实现 + * + * @author looly + * + */ +package cn.hutool.cron.task; \ No newline at end of file diff --git a/hutool-cron/src/test/java/cn/hutool/cron/demo/AddAndRemoveMainTest.java b/hutool-cron/src/test/java/cn/hutool/cron/demo/AddAndRemoveMainTest.java new file mode 100644 index 000000000..a3fee6cbb --- /dev/null +++ b/hutool-cron/src/test/java/cn/hutool/cron/demo/AddAndRemoveMainTest.java @@ -0,0 +1,32 @@ +package cn.hutool.cron.demo; + +import cn.hutool.core.lang.Console; +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.cron.CronUtil; + +public class AddAndRemoveMainTest { + + public static void main(String[] args) { + CronUtil.setMatchSecond(true); + CronUtil.start(false); + CronUtil.getScheduler().clear(); + String id = CronUtil.schedule("*/2 * * * * *", new Runnable() { + + @Override + public void run() { + Console.log("task running : 2s"); + } + }); + ThreadUtil.sleep(3000); + CronUtil.remove(id); + Console.log("Task Removed"); + id = CronUtil.schedule("*/3 * * * * *", new Runnable() { + + @Override + public void run() { + Console.log("New task add running : 3s"); + } + }); + Console.log("New Task added."); + } +} diff --git a/hutool-cron/src/test/java/cn/hutool/cron/demo/CronTest.java b/hutool-cron/src/test/java/cn/hutool/cron/demo/CronTest.java new file mode 100644 index 000000000..8192a787c --- /dev/null +++ b/hutool-cron/src/test/java/cn/hutool/cron/demo/CronTest.java @@ -0,0 +1,72 @@ +package cn.hutool.cron.demo; + +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.lang.Console; +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.cron.CronUtil; +import cn.hutool.cron.task.Task; + +/** + * 定时任务样例 + */ +public class CronTest { + + @Test + @Ignore + public void customCronTest() { + CronUtil.schedule("*/2 * * * * *", new Task() { + + @Override + public void execute() { + Console.log("Task excuted."); + } + }); + + // 支持秒级别定时任务 + CronUtil.setMatchSecond(true); + CronUtil.start(); + } + + @Test + @Ignore + public void cronTest() { + // 支持秒级别定时任务 + CronUtil.setMatchSecond(true); + CronUtil.getScheduler().setDaemon(false); + CronUtil.start(); + + ThreadUtil.sleep(3000); + CronUtil.stop(); + } + + @Test + @Ignore + public void cronTest2() { + // 支持秒级别定时任务 + CronUtil.setMatchSecond(true); + CronUtil.start(); + + ThreadUtil.sleep(30000); + } + + @Test +// @Ignore + public void addAndRemoveTest() { + String id = CronUtil.schedule("*/2 * * * * *", new Runnable() { + + @Override + public void run() { + Console.log("task running : 2s"); + } + }); + + Console.log(id); + CronUtil.remove(id); + + // 支持秒级别定时任务 + CronUtil.setMatchSecond(true); + CronUtil.start(); + } +} diff --git a/hutool-cron/src/test/java/cn/hutool/cron/demo/DeamonMainTest.java b/hutool-cron/src/test/java/cn/hutool/cron/demo/DeamonMainTest.java new file mode 100644 index 000000000..106eef898 --- /dev/null +++ b/hutool-cron/src/test/java/cn/hutool/cron/demo/DeamonMainTest.java @@ -0,0 +1,19 @@ +package cn.hutool.cron.demo; + +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.cron.CronUtil; +import cn.hutool.cron.task.InvokeTask; + +public class DeamonMainTest { + public static void main(String[] args) { + // 测试守护线程是否对作业线程有效 + CronUtil.schedule("*/2 * * * * *", new InvokeTask("cn.hutool.cron.demo.TestJob.doWhileTest")); + // 当为守护线程时,stop方法调用后doWhileTest里的循环输出将终止,表示作业线程正常结束 + // 当非守护线程时,stop方法调用后,不再产生新的作业,原作业正常执行。 + CronUtil.setMatchSecond(true); + CronUtil.start(true); + + ThreadUtil.sleep(3000); + CronUtil.stop(); + } +} diff --git a/hutool-cron/src/test/java/cn/hutool/cron/demo/JobMainTest.java b/hutool-cron/src/test/java/cn/hutool/cron/demo/JobMainTest.java new file mode 100644 index 000000000..e02c7f6a3 --- /dev/null +++ b/hutool-cron/src/test/java/cn/hutool/cron/demo/JobMainTest.java @@ -0,0 +1,14 @@ +package cn.hutool.cron.demo; + +import cn.hutool.cron.CronUtil; + +/** + * 定时任务样例 + */ +public class JobMainTest { + + public static void main(String[] args) { + CronUtil.setMatchSecond(true); + CronUtil.start(false); + } +} diff --git a/hutool-cron/src/test/java/cn/hutool/cron/demo/TestJob.java b/hutool-cron/src/test/java/cn/hutool/cron/demo/TestJob.java new file mode 100644 index 000000000..74d9257e5 --- /dev/null +++ b/hutool-cron/src/test/java/cn/hutool/cron/demo/TestJob.java @@ -0,0 +1,36 @@ +package cn.hutool.cron.demo; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.lang.Console; +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.core.util.IdUtil; + +/** + * 测试定时任务,当触发到定时的时间点时,执行doTest方法 + * + * @author looly + * + */ +public class TestJob { + + private String jobId = IdUtil.simpleUUID(); + + /** + * 执行定时任务内容 + */ + public void doTest() { +// String name = Thread.currentThread().getName(); + Console.log("Test Job {} running... at {}", jobId, DateUtil.now()); + } + + /** + * 执行循环定时任务,测试在定时任务结束时作为deamon线程是否能正常结束 + */ + public void doWhileTest() { + String name = Thread.currentThread().getName(); + while (true) { + Console.log("Job {} while running...", name); + ThreadUtil.sleep(2000); + } + } +} diff --git a/hutool-cron/src/test/java/cn/hutool/cron/demo/TestJob2.java b/hutool-cron/src/test/java/cn/hutool/cron/demo/TestJob2.java new file mode 100644 index 000000000..7d71dc31f --- /dev/null +++ b/hutool-cron/src/test/java/cn/hutool/cron/demo/TestJob2.java @@ -0,0 +1,24 @@ +package cn.hutool.cron.demo; + +import java.util.concurrent.TimeUnit; + +import cn.hutool.core.lang.Console; +import cn.hutool.core.thread.ThreadUtil; + +/** + * 测试定时任务,当触发到定时的时间点时,执行doTest方法 + * + * @author looly + * + */ +public class TestJob2 { + + /** + * 执行定时任务内容 + */ + public void doTest() { + Console.log("TestJob2.doTest开始执行……"); + ThreadUtil.sleep(20, TimeUnit.SECONDS); + Console.log("延迟20s打印testJob2"); + } +} diff --git a/hutool-cron/src/test/java/cn/hutool/cron/pattern/CronPatternTest.java b/hutool-cron/src/test/java/cn/hutool/cron/pattern/CronPatternTest.java new file mode 100644 index 000000000..6939e49fa --- /dev/null +++ b/hutool-cron/src/test/java/cn/hutool/cron/pattern/CronPatternTest.java @@ -0,0 +1,138 @@ +package cn.hutool.cron.pattern; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.cron.pattern.CronPattern; + +/** + * 定时任务单元测试类 + * + * @author Looly + * + */ +public class CronPatternTest { + + @Test + public void matchAllTest() { + CronPattern pattern; + // 任何时间匹配 + pattern = new CronPattern("* * * * * *"); + Assert.assertTrue(pattern.match(DateUtil.current(false), true)); + Assert.assertTrue(pattern.match(DateUtil.current(false), false)); + } + + @Test + public void matchAllTest2() { + // 在5位表达式中,秒部分并不是任意匹配,而是一个固定值 + // 因此此处匹配就不能匹配秒 + CronPattern pattern; + // 任何时间匹配 + pattern = new CronPattern("* * * * *"); + for(int i = 0; i < 1; i++) { + Assert.assertTrue(pattern.match(DateUtil.current(false), false)); + } + } + + @Test + public void cronPatternTest() { + CronPattern pattern; + + // 12:11匹配 + pattern = new CronPattern("39 11 12 * * *"); + assertMatch(pattern, "12:11:39"); + + // 每5分钟匹配,匹配分钟为:[0,5,10,15,20,25,30,35,40,45,50,55] + pattern = new CronPattern("39 */5 * * * *"); + assertMatch(pattern, "12:00:39"); + assertMatch(pattern, "12:05:39"); + assertMatch(pattern, "12:10:39"); + assertMatch(pattern, "12:15:39"); + assertMatch(pattern, "12:20:39"); + assertMatch(pattern, "12:25:39"); + assertMatch(pattern, "12:30:39"); + assertMatch(pattern, "12:35:39"); + assertMatch(pattern, "12:40:39"); + assertMatch(pattern, "12:45:39"); + assertMatch(pattern, "12:50:39"); + assertMatch(pattern, "12:55:39"); + + // 2:01,3:01,4:01 + pattern = new CronPattern("39 1 2-4 * * *"); + assertMatch(pattern, "02:01:39"); + assertMatch(pattern, "03:01:39"); + assertMatch(pattern, "04:01:39"); + + // 2:01,3:01,4:01 + pattern = new CronPattern("39 1 2,3,4 * * *"); + assertMatch(pattern, "02:01:39"); + assertMatch(pattern, "03:01:39"); + assertMatch(pattern, "04:01:39"); + + // 08-07, 08-06 + pattern = new CronPattern("39 0 0 6,7 8 *"); + assertMatch(pattern, "2016-08-07 00:00:39"); + assertMatch(pattern, "2016-08-06 00:00:39"); + + // 别名忽略大小写 + pattern = new CronPattern("39 0 0 6,7 Aug *"); + assertMatch(pattern, "2016-08-06 00:00:39"); + assertMatch(pattern, "2016-08-07 00:00:39"); + + pattern = new CronPattern("39 0 0 7 aug *"); + assertMatch(pattern, "2016-08-07 00:00:39"); + + // 星期四 + pattern = new CronPattern("39 0 0 * * Thu"); + assertMatch(pattern, "2017-02-09 00:00:39"); + assertMatch(pattern, "2017-02-09 00:00:39"); + + } + + @Test + public void CronPatternTest2() { + CronPattern pattern = new CronPattern("0/30 * * * *"); + Assert.assertTrue(pattern.match(DateUtil.parse("2018-10-09 12:00:00").getTime(), false)); + Assert.assertTrue(pattern.match(DateUtil.parse("2018-10-09 12:30:00").getTime(), false)); + + pattern = new CronPattern("32 * * * *"); + Assert.assertTrue(pattern.match(DateUtil.parse("2018-10-09 12:32:00").getTime(), false)); + } + + @Test + public void patternTest() { + CronPattern pattern = new CronPattern("* 0 4 * * ?"); + assertMatch(pattern, "2017-02-09 04:00:00"); + assertMatch(pattern, "2017-02-19 04:00:33"); + + // 6位Quartz风格表达式 + pattern = new CronPattern("* 0 4 * * ?"); + assertMatch(pattern, "2017-02-09 04:00:00"); + assertMatch(pattern, "2017-02-19 04:00:33"); + } + + @Test + public void rangePatternTest() { + CronPattern pattern = new CronPattern("* 20/2 * * * ?"); + assertMatch(pattern, "2017-02-09 04:20:00"); + assertMatch(pattern, "2017-02-09 05:20:00"); + assertMatch(pattern, "2017-02-19 04:22:33"); + + pattern = new CronPattern("* 2-20/2 * * * ?"); + assertMatch(pattern, "2017-02-09 04:02:00"); + assertMatch(pattern, "2017-02-09 05:04:00"); + assertMatch(pattern, "2017-02-19 04:20:33"); + } + + /** + * 表达式是否匹配日期 + * + * @param pattern 表达式 + * @param date 日期,标准日期时间字符串 + */ + private void assertMatch(CronPattern pattern, String date) { + Assert.assertTrue(pattern.match(DateUtil.parse(date).getTime(), false)); + Assert.assertTrue(pattern.match(DateUtil.parse(date).getTime(), true)); + } +} diff --git a/hutool-cron/src/test/java/cn/hutool/cron/pattern/CronPatternUtilTest.java b/hutool-cron/src/test/java/cn/hutool/cron/pattern/CronPatternUtilTest.java new file mode 100644 index 000000000..c06586cbf --- /dev/null +++ b/hutool-cron/src/test/java/cn/hutool/cron/pattern/CronPatternUtilTest.java @@ -0,0 +1,48 @@ +package cn.hutool.cron.pattern; + +import java.util.Date; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.date.DateUtil; + +public class CronPatternUtilTest { + + @Test + public void matchedDatesTest() { + //测试每30秒执行 + List matchedDates = CronPatternUtil.matchedDates("0/30 * 8-18 * * ?", DateUtil.parse("2018-10-15 14:33:22"), 5, true); + Assert.assertEquals(5, matchedDates.size()); + Assert.assertEquals("2018-10-15 14:33:30", matchedDates.get(0).toString()); + Assert.assertEquals("2018-10-15 14:34:00", matchedDates.get(1).toString()); + Assert.assertEquals("2018-10-15 14:34:30", matchedDates.get(2).toString()); + Assert.assertEquals("2018-10-15 14:35:00", matchedDates.get(3).toString()); + Assert.assertEquals("2018-10-15 14:35:30", matchedDates.get(4).toString()); + } + + @Test + public void matchedDatesTest2() { + //测试每小时执行 + List matchedDates = CronPatternUtil.matchedDates("0 0 */1 * * *", DateUtil.parse("2018-10-15 14:33:22"), 5, true); + Assert.assertEquals(5, matchedDates.size()); + Assert.assertEquals("2018-10-15 15:00:00", matchedDates.get(0).toString()); + Assert.assertEquals("2018-10-15 16:00:00", matchedDates.get(1).toString()); + Assert.assertEquals("2018-10-15 17:00:00", matchedDates.get(2).toString()); + Assert.assertEquals("2018-10-15 18:00:00", matchedDates.get(3).toString()); + Assert.assertEquals("2018-10-15 19:00:00", matchedDates.get(4).toString()); + } + + @Test + public void matchedDatesTest3() { + //测试最后一天 + List matchedDates = CronPatternUtil.matchedDates("0 0 */1 L * *", DateUtil.parse("2018-10-30 23:33:22"), 5, true); + Assert.assertEquals(5, matchedDates.size()); + Assert.assertEquals("2018-10-31 00:00:00", matchedDates.get(0).toString()); + Assert.assertEquals("2018-10-31 01:00:00", matchedDates.get(1).toString()); + Assert.assertEquals("2018-10-31 02:00:00", matchedDates.get(2).toString()); + Assert.assertEquals("2018-10-31 03:00:00", matchedDates.get(3).toString()); + Assert.assertEquals("2018-10-31 04:00:00", matchedDates.get(4).toString()); + } +} diff --git a/hutool-cron/src/test/resources/config/cron.setting b/hutool-cron/src/test/resources/config/cron.setting new file mode 100644 index 000000000..d835ac8e4 --- /dev/null +++ b/hutool-cron/src/test/resources/config/cron.setting @@ -0,0 +1,17 @@ +#------------------------------------------------------------------ +# 定时任务配置文件 +# 定时任务表达分为以下几种情况: +# 1. 表达式为5位,此时兼容Linux的Crontab模式,第一位匹配分,此时如果为秒匹配模式,则秒部分为固定值(取决于加入表达式时当前时间秒数) +# 2. 表达式为6位,此时兼容Quartz模式,第一位匹配秒,但是只有秒匹配模式时秒部分定义才有效 +# 3. 表达式为7位,此时兼容Quartz模式,第一位匹配秒,最后一位匹配年 +#------------------------------------------------------------------ + +# cn.hutool.cron.demo.TestJob.doTest = */1 * * * * * + +[cn.hutool.cron.demo] +# 6位表达式在秒匹配模式下可用,此处表示每秒执行一次 +# TestJob.doTest = */1 * * * * * +# 5位表达式在分匹配模式下可用,此处表示每分钟执行一次 +# 如果此时为秒匹配模式,则秒部分为固定数字(此秒取决于加入表达式当前时间的秒数) +TestJob.doTest = 0/30 * 8-18 * * ? +TestJob2.doTest = */3 * * * * * \ No newline at end of file diff --git a/hutool-crypto/pom.xml b/hutool-crypto/pom.xml new file mode 100644 index 000000000..53f3312ea --- /dev/null +++ b/hutool-crypto/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + jar + + + cn.hutool + hutool-parent + 4.6.2-SNAPSHOT + + + hutool-crypto + ${project.artifactId} + Hutool 加密解密 + + + + 1.60 + + + + + cn.hutool + hutool-core + ${project.parent.version} + + + org.bouncycastle + bcprov-jdk15on + ${bouncycastle.version} + compile + true + + + diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/BCUtil.java b/hutool-crypto/src/main/java/cn/hutool/crypto/BCUtil.java new file mode 100644 index 000000000..ab2e849f5 --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/BCUtil.java @@ -0,0 +1,168 @@ +package cn.hutool.crypto; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.Certificate; +import java.security.spec.ECFieldFp; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.EllipticCurve; + +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; +import org.bouncycastle.jce.ECNamedCurveTable; +import org.bouncycastle.jce.ECPointUtil; +import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec; +import org.bouncycastle.jce.spec.ECNamedCurveSpec; +import org.bouncycastle.math.ec.ECCurve; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemReader; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; + +/** + * Bouncy Castle相关工具类封装 + * + * @author looly + * @since 4.5.0 + */ +public class BCUtil { + /** + * 编码压缩EC公钥(基于BouncyCastle)
+ * 见:https://www.cnblogs.com/xinzhao/p/8963724.html + * + * @param publicKey {@link PublicKey},必须为org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey + * @return 压缩得到的X + * @since 4.4.4 + */ + public static byte[] encodeECPublicKey(PublicKey publicKey) { + return ((BCECPublicKey) publicKey).getQ().getEncoded(true); + } + + /** + * 解码恢复EC压缩公钥,支持Base64和Hex编码,(基于BouncyCastle)
+ * 见:https://www.cnblogs.com/xinzhao/p/8963724.html + * + * @param encode 压缩公钥 + * @param curveName EC曲线名 + * @since 4.4.4 + */ + public static PublicKey decodeECPoint(String encode, String curveName) { + return decodeECPoint(SecureUtil.decode(encode), curveName); + } + + /** + * 解码恢复EC压缩公钥,支持Base64和Hex编码,(基于BouncyCastle)
+ * 见:https://www.cnblogs.com/xinzhao/p/8963724.html + * + * @param encodeByte 压缩公钥 + * @param curveName EC曲线名,例如{@link KeyUtil#SM2_DEFAULT_CURVE} + * @since 4.4.4 + */ + public static PublicKey decodeECPoint(byte[] encodeByte, String curveName) { + final ECNamedCurveParameterSpec namedSpec = ECNamedCurveTable.getParameterSpec(curveName); + final ECCurve curve = namedSpec.getCurve(); + final EllipticCurve ecCurve = new EllipticCurve(// + new ECFieldFp(curve.getField().getCharacteristic()), // + curve.getA().toBigInteger(), // + curve.getB().toBigInteger()); + // 根据X恢复点Y + final ECPoint point = ECPointUtil.decodePoint(ecCurve, encodeByte); + + // 根据曲线恢复公钥格式 + ECParameterSpec ecSpec = new ECNamedCurveSpec(curveName, curve, namedSpec.getG(), namedSpec.getN()); + + final KeyFactory PubKeyGen = KeyUtil.getKeyFactory("EC"); + try { + return PubKeyGen.generatePublic(new ECPublicKeySpec(point, ecSpec)); + } catch (GeneralSecurityException e) { + throw new CryptoException(e); + } + } + + /** + * 读取PEM格式的私钥 + * + * @param pemStream pem流 + * @return {@link PrivateKey} + * @since 4.5.2 + */ + public static PrivateKey readPrivateKey(InputStream pemStream) { + return KeyUtil.generateRSAPrivateKey(readKeyBytes(pemStream)); + } + + /** + * 读取PEM格式的公钥 + * + * @param pemStream pem流 + * @return {@link PublicKey} + * @since 4.5.2 + */ + public static PublicKey readPublicKey(InputStream pemStream) { + final Certificate certificate = KeyUtil.readX509Certificate(pemStream); + if(null == certificate) { + return null; + } + return certificate.getPublicKey(); + } + + /** + * 从pem文件中读取公钥或私钥
+ * 根据类型返回{@link PublicKey} 或者 {@link PrivateKey} + * + * @param keyStream pem流 + * @return {@link Key} + * @since 4.5.2 + */ + public static Key readKey(InputStream keyStream) { + final PemObject object = readPemObject(keyStream); + final String type = object.getType(); + if (StrUtil.isNotBlank(type) && type.endsWith("PRIVATE KEY")) { + return KeyUtil.generateRSAPrivateKey(object.getContent()); + } else { + return KeyUtil.readX509Certificate(keyStream).getPublicKey(); + } + } + + /** + * 从pem文件中读取公钥或私钥 + * + * @param keyStream pem流 + * @return 密钥bytes + * @since 4.5.2 + */ + public static byte[] readKeyBytes(InputStream keyStream) { + PemObject pemObject = readPemObject(keyStream); + if (null != pemObject) { + return pemObject.getContent(); + } + return null; + } + + /** + * 读取pem文件中的信息,包括类型、头信息和密钥内容 + * + * @param keyStream pem流 + * @return {@link PemObject} + * @since 4.5.2 + */ + public static PemObject readPemObject(InputStream keyStream) { + PemReader pemReader = null; + try { + pemReader = new PemReader(IoUtil.getReader(keyStream, CharsetUtil.CHARSET_UTF_8)); + return pemReader.readPemObject(); + } catch (IOException e) { + throw new IORuntimeException(e); + } finally { + IoUtil.close(pemReader); + } + } +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/CryptoException.java b/hutool-crypto/src/main/java/cn/hutool/crypto/CryptoException.java new file mode 100644 index 000000000..73c082acd --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/CryptoException.java @@ -0,0 +1,33 @@ +package cn.hutool.crypto; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 加密异常 + * @author Looly + * + */ +public class CryptoException extends RuntimeException { + private static final long serialVersionUID = 8068509879445395353L; + + public CryptoException(Throwable e) { + super(ExceptionUtil.getMessage(e), e); + } + + public CryptoException(String message) { + super(message); + } + + public CryptoException(String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params)); + } + + public CryptoException(String message, Throwable throwable) { + super(message, throwable); + } + + public CryptoException(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/GlobalBouncyCastleProvider.java b/hutool-crypto/src/main/java/cn/hutool/crypto/GlobalBouncyCastleProvider.java new file mode 100644 index 000000000..0f0f208a0 --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/GlobalBouncyCastleProvider.java @@ -0,0 +1,42 @@ +package cn.hutool.crypto; + +import java.security.Provider; + +/** + * 全局单例的 org.bouncycastle.jce.provider.BouncyCastleProvider 对象 + * @author looly + * + */ +public enum GlobalBouncyCastleProvider { + INSTANCE; + + private Provider provider; + private static boolean useBouncyCastle = true; + + private GlobalBouncyCastleProvider() { + try { + this.provider = ProviderFactory.createBouncyCastleProvider(); + } catch (NoClassDefFoundError e) { + // ignore + } + } + + /** + * 获取{@link Provider} + * @return {@link Provider} + */ + public Provider getProvider() { + return useBouncyCastle ? this.provider : null; + } + + /** + * 设置是否使用Bouncy Castle库
+ * 如果设置为false,表示强制关闭Bouncy Castle而使用JDK + * + * @param isUseBouncyCastle + * @since 4.5.2 + */ + public static void setUseBouncyCastle(boolean isUseBouncyCastle) { + useBouncyCastle = isUseBouncyCastle; + } +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/KeyUtil.java b/hutool-crypto/src/main/java/cn/hutool/crypto/KeyUtil.java new file mode 100644 index 000000000..09f05aeb0 --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/KeyUtil.java @@ -0,0 +1,770 @@ +package cn.hutool.crypto; + +import java.io.InputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.DESKeySpec; +import javax.crypto.spec.DESedeKeySpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.asymmetric.AsymmetricAlgorithm; +import cn.hutool.crypto.symmetric.SymmetricAlgorithm; + +/** + * 密钥工具类 + * + * @author looly, Gsealy + * @since 4.4.1 + */ +public class KeyUtil { + + /** Java密钥库(Java Key Store,JKS)KEY_STORE */ + public static final String KEY_STORE = "JKS"; + public static final String X509 = "X.509"; + + /** + * 默认密钥字节数 + * + *
+	 * RSA/DSA
+	 * Default Keysize 1024
+	 * Keysize must be a multiple of 64, ranging from 512 to 1024 (inclusive).
+	 * 
+ */ + public static final int DEFAULT_KEY_SIZE = 1024; + + /** + * SM2默认曲线 + * + *
+	 * Default SM2 curve
+	 * 
+ */ + public static final String SM2_DEFAULT_CURVE = "sm2p256v1"; + + /** + * 生成 {@link SecretKey},仅用于对称加密和摘要算法密钥生成 + * + * @param algorithm 算法,支持PBE算法 + * @return {@link SecretKey} + */ + public static SecretKey generateKey(String algorithm) { + return generateKey(algorithm, -1); + } + + /** + * 生成 {@link SecretKey},仅用于对称加密和摘要算法密钥生成 + * + * @param algorithm 算法,支持PBE算法 + * @param keySize 密钥长度 + * @return {@link SecretKey} + * @since 3.1.2 + */ + public static SecretKey generateKey(String algorithm, int keySize) { + algorithm = getMainAlgorithm(algorithm); + + final KeyGenerator keyGenerator = getKeyGenerator(algorithm); + if (keySize > 0) { + keyGenerator.init(keySize); + } else if (SymmetricAlgorithm.AES.getValue().equals(algorithm)) { + // 对于AES的密钥,除非指定,否则强制使用128位 + keyGenerator.init(128); + } + return keyGenerator.generateKey(); + } + + /** + * 生成 {@link SecretKey},仅用于对称加密和摘要算法密钥生成 + * + * @param algorithm 算法 + * @param key 密钥,如果为{@code null} 自动生成随机密钥 + * @return {@link SecretKey} + */ + public static SecretKey generateKey(String algorithm, byte[] key) { + Assert.notBlank(algorithm, "Algorithm is blank!"); + SecretKey secretKey = null; + if (algorithm.startsWith("PBE")) { + // PBE密钥 + secretKey = generatePBEKey(algorithm, (null == key) ? null : StrUtil.str(key, CharsetUtil.CHARSET_UTF_8).toCharArray()); + } else if (algorithm.startsWith("DES")) { + // DES密钥 + secretKey = generateDESKey(algorithm, key); + } else { + // 其它算法密钥 + secretKey = (null == key) ? generateKey(algorithm) : new SecretKeySpec(key, algorithm); + } + return secretKey; + } + + /** + * 生成 {@link SecretKey} + * + * @param algorithm DES算法,包括DES、DESede等 + * @param key 密钥 + * @return {@link SecretKey} + */ + public static SecretKey generateDESKey(String algorithm, byte[] key) { + if (StrUtil.isBlank(algorithm) || false == algorithm.startsWith("DES")) { + throw new CryptoException("Algorithm [{}] is not a DES algorithm!"); + } + + SecretKey secretKey = null; + if (null == key) { + secretKey = generateKey(algorithm); + } else { + KeySpec keySpec; + try { + if (algorithm.startsWith("DESede")) { + // DESede兼容 + keySpec = new DESedeKeySpec(key); + } else { + keySpec = new DESKeySpec(key); + } + } catch (InvalidKeyException e) { + throw new CryptoException(e); + } + secretKey = generateKey(algorithm, keySpec); + } + return secretKey; + } + + /** + * 生成PBE {@link SecretKey} + * + * @param algorithm PBE算法,包括:PBEWithMD5AndDES、PBEWithSHA1AndDESede、PBEWithSHA1AndRC2_40等 + * @param key 密钥 + * @return {@link SecretKey} + */ + public static SecretKey generatePBEKey(String algorithm, char[] key) { + if (StrUtil.isBlank(algorithm) || false == algorithm.startsWith("PBE")) { + throw new CryptoException("Algorithm [{}] is not a PBE algorithm!"); + } + + if (null == key) { + key = RandomUtil.randomString(32).toCharArray(); + } + PBEKeySpec keySpec = new PBEKeySpec(key); + return generateKey(algorithm, keySpec); + } + + /** + * 生成 {@link SecretKey},仅用于对称加密和摘要算法 + * + * @param algorithm 算法 + * @param keySpec {@link KeySpec} + * @return {@link SecretKey} + */ + public static SecretKey generateKey(String algorithm, KeySpec keySpec) { + final SecretKeyFactory keyFactory = getSecretKeyFactory(algorithm); + try { + return keyFactory.generateSecret(keySpec); + } catch (InvalidKeySpecException e) { + throw new CryptoException(e); + } + } + + /** + * 生成RSA私钥,仅用于非对称加密
+ * 采用PKCS#8规范,此规范定义了私钥信息语法和加密私钥语法
+ * 算法见:https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyFactory + * + * @param key 密钥,必须为DER编码存储 + * @return RSA私钥 {@link PrivateKey} + * @since 4.5.2 + */ + public static PrivateKey generateRSAPrivateKey(byte[] key) { + return generatePrivateKey(AsymmetricAlgorithm.RSA.getValue(), key); + } + + /** + * 生成私钥,仅用于非对称加密
+ * 采用PKCS#8规范,此规范定义了私钥信息语法和加密私钥语法
+ * 算法见:https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyFactory + * + * @param algorithm 算法 + * @param key 密钥,必须为DER编码存储 + * @return 私钥 {@link PrivateKey} + */ + public static PrivateKey generatePrivateKey(String algorithm, byte[] key) { + if (null == key) { + return null; + } + return generatePrivateKey(algorithm, new PKCS8EncodedKeySpec(key)); + } + + /** + * 生成私钥,仅用于非对称加密
+ * 算法见:https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyFactory + * + * @param algorithm 算法 + * @param keySpec {@link KeySpec} + * @return 私钥 {@link PrivateKey} + * @since 3.1.1 + */ + public static PrivateKey generatePrivateKey(String algorithm, KeySpec keySpec) { + if (null == keySpec) { + return null; + } + algorithm = getAlgorithmAfterWith(algorithm); + try { + return getKeyFactory(algorithm).generatePrivate(keySpec); + } catch (Exception e) { + throw new CryptoException(e); + } + } + + /** + * 生成私钥,仅用于非对称加密 + * + * @param keyStore {@link KeyStore} + * @param alias 别名 + * @param password 密码 + * @return 私钥 {@link PrivateKey} + */ + public static PrivateKey generatePrivateKey(KeyStore keyStore, String alias, char[] password) { + try { + return (PrivateKey) keyStore.getKey(alias, password); + } catch (Exception e) { + throw new CryptoException(e); + } + } + + /** + * 生成RSA公钥,仅用于非对称加密
+ * 采用X509证书规范
+ * 算法见:https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyFactory + * + * @param key 密钥,必须为DER编码存储 + * @return 公钥 {@link PublicKey} + * @since 4.5.2 + */ + public static PublicKey generateRSAPublicKey(byte[] key) { + return generatePublicKey(AsymmetricAlgorithm.RSA.getValue(), key); + } + + /** + * 生成公钥,仅用于非对称加密
+ * 采用X509证书规范
+ * 算法见:https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyFactory + * + * @param algorithm 算法 + * @param key 密钥,必须为DER编码存储 + * @return 公钥 {@link PublicKey} + */ + public static PublicKey generatePublicKey(String algorithm, byte[] key) { + if (null == key) { + return null; + } + return generatePublicKey(algorithm, new X509EncodedKeySpec(key)); + } + + /** + * 生成公钥,仅用于非对称加密
+ * 算法见:https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyFactory + * + * @param algorithm 算法 + * @param keySpec {@link KeySpec} + * @return 公钥 {@link PublicKey} + * @since 3.1.1 + */ + public static PublicKey generatePublicKey(String algorithm, KeySpec keySpec) { + if (null == keySpec) { + return null; + } + algorithm = getAlgorithmAfterWith(algorithm); + try { + return getKeyFactory(algorithm).generatePublic(keySpec); + } catch (Exception e) { + throw new CryptoException(e); + } + } + + /** + * 生成用于非对称加密的公钥和私钥,仅用于非对称加密
+ * 密钥对生成算法见:https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyPairGenerator + * + * @param algorithm 非对称加密算法 + * @return {@link KeyPair} + */ + public static KeyPair generateKeyPair(String algorithm) { + return generateKeyPair(algorithm, DEFAULT_KEY_SIZE); + } + + /** + * 生成用于非对称加密的公钥和私钥
+ * 密钥对生成算法见:https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyPairGenerator + * + * @param algorithm 非对称加密算法 + * @param keySize 密钥模(modulus )长度 + * @return {@link KeyPair} + */ + public static KeyPair generateKeyPair(String algorithm, int keySize) { + return generateKeyPair(algorithm, keySize, null); + } + + /** + * 生成用于非对称加密的公钥和私钥
+ * 密钥对生成算法见:https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyPairGenerator + * + * @param algorithm 非对称加密算法 + * @param keySize 密钥模(modulus )长度 + * @param seed 种子 + * @return {@link KeyPair} + */ + public static KeyPair generateKeyPair(String algorithm, int keySize, byte[] seed) { + // SM2算法需要单独定义其曲线生成 + if ("SM2".equalsIgnoreCase(algorithm)) { + final ECGenParameterSpec sm2p256v1 = new ECGenParameterSpec(SM2_DEFAULT_CURVE); + return generateKeyPair(algorithm, keySize, seed, sm2p256v1); + } + + return generateKeyPair(algorithm, keySize, seed, (AlgorithmParameterSpec[]) null); + } + + /** + * 生成用于非对称加密的公钥和私钥
+ * 密钥对生成算法见:https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyPairGenerator + * + * @param algorithm 非对称加密算法 + * @param params {@link AlgorithmParameterSpec} + * @return {@link KeyPair} + * @since 4.3.3 + */ + public static KeyPair generateKeyPair(String algorithm, AlgorithmParameterSpec params) { + return generateKeyPair(algorithm, null, params); + } + + /** + * 生成用于非对称加密的公钥和私钥
+ * 密钥对生成算法见:https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyPairGenerator + * + * @param algorithm 非对称加密算法 + * @param param {@link AlgorithmParameterSpec} + * @param seed 种子 + * @return {@link KeyPair} + * @since 4.3.3 + */ + public static KeyPair generateKeyPair(String algorithm, byte[] seed, AlgorithmParameterSpec param) { + return generateKeyPair(algorithm, DEFAULT_KEY_SIZE, seed, param); + } + + /** + * 生成用于非对称加密的公钥和私钥
+ * 密钥对生成算法见:https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyPairGenerator + * + * @param algorithm 非对称加密算法 + * @param keySize 密钥模(modulus )长度 + * @param seed 种子 + * @param params {@link AlgorithmParameterSpec} + * @return {@link KeyPair} + * @since 4.3.3 + */ + public static KeyPair generateKeyPair(String algorithm, int keySize, byte[] seed, AlgorithmParameterSpec... params) { + algorithm = getAlgorithmAfterWith(algorithm); + final KeyPairGenerator keyPairGen = getKeyPairGenerator(algorithm); + + // 密钥模(modulus )长度初始化定义 + if (keySize > 0) { + // key长度适配修正 + if ("EC".equalsIgnoreCase(algorithm) && keySize > 256) { + // 对于EC算法,密钥长度有限制,在此使用默认256 + keySize = 256; + } + if (null != seed) { + keyPairGen.initialize(keySize, new SecureRandom(seed)); + } else { + keyPairGen.initialize(keySize); + } + } + + // 自定义初始化参数 + if (ArrayUtil.isNotEmpty(params)) { + for (AlgorithmParameterSpec param : params) { + if (null == param) { + continue; + } + try { + if (null != seed) { + keyPairGen.initialize(param, new SecureRandom(seed)); + } else { + keyPairGen.initialize(param); + } + } catch (InvalidAlgorithmParameterException e) { + throw new CryptoException(e); + } + } + } + return keyPairGen.generateKeyPair(); + } + + /** + * 获取{@link KeyPairGenerator} + * + * @param algorithm 非对称加密算法 + * @return {@link KeyPairGenerator} + * @since 4.4.3 + */ + public static KeyPairGenerator getKeyPairGenerator(String algorithm) { + final Provider provider = GlobalBouncyCastleProvider.INSTANCE.getProvider(); + + KeyPairGenerator keyPairGen; + try { + keyPairGen = (null == provider) // + ? KeyPairGenerator.getInstance(getMainAlgorithm(algorithm)) // + : KeyPairGenerator.getInstance(getMainAlgorithm(algorithm), provider);// + } catch (NoSuchAlgorithmException e) { + throw new CryptoException(e); + } + return keyPairGen; + } + + /** + * 获取{@link KeyFactory} + * + * @param algorithm 非对称加密算法 + * @return {@link KeyFactory} + * @since 4.4.4 + */ + public static KeyFactory getKeyFactory(String algorithm) { + final Provider provider = GlobalBouncyCastleProvider.INSTANCE.getProvider(); + + KeyFactory keyFactory; + try { + keyFactory = (null == provider) // + ? KeyFactory.getInstance(getMainAlgorithm(algorithm)) // + : KeyFactory.getInstance(getMainAlgorithm(algorithm), provider); + } catch (NoSuchAlgorithmException e) { + throw new CryptoException(e); + } + return keyFactory; + } + + /** + * 获取{@link SecretKeyFactory} + * + * @param algorithm 对称加密算法 + * @return {@link KeyFactory} + * @since 4.5.2 + */ + public static SecretKeyFactory getSecretKeyFactory(String algorithm) { + final Provider provider = GlobalBouncyCastleProvider.INSTANCE.getProvider(); + + SecretKeyFactory keyFactory; + try { + keyFactory = (null == provider) // + ? SecretKeyFactory.getInstance(getMainAlgorithm(algorithm)) // + : SecretKeyFactory.getInstance(getMainAlgorithm(algorithm), provider); + } catch (NoSuchAlgorithmException e) { + throw new CryptoException(e); + } + return keyFactory; + } + + /** + * 获取{@link KeyGenerator} + * + * @param algorithm 对称加密算法 + * @return {@link KeyGenerator} + * @since 4.5.2 + */ + public static KeyGenerator getKeyGenerator(String algorithm) { + final Provider provider = GlobalBouncyCastleProvider.INSTANCE.getProvider(); + + KeyGenerator generator; + try { + generator = (null == provider) // + ? KeyGenerator.getInstance(getMainAlgorithm(algorithm)) // + : KeyGenerator.getInstance(getMainAlgorithm(algorithm), provider); + } catch (NoSuchAlgorithmException e) { + throw new CryptoException(e); + } + return generator; + } + + /** + * 获取主体算法名,例如RSA/ECB/PKCS1Padding的主体算法是RSA + * + * @return 主体算法名 + * @since 4.5.2 + */ + public static String getMainAlgorithm(String algorithm) { + final int slashIndex = algorithm.indexOf(CharUtil.SLASH); + if (slashIndex > 0) { + return algorithm.substring(0, slashIndex); + } + return algorithm; + } + + /** + * 获取用于密钥生成的算法
+ * 获取XXXwithXXX算法的后半部分算法,如果为ECDSA或SM2,返回算法为EC + * + * @param algorithm XXXwithXXX算法 + * @return 算法 + */ + public static String getAlgorithmAfterWith(String algorithm) { + Assert.notNull(algorithm, "algorithm must be not null !"); + int indexOfWith = StrUtil.lastIndexOfIgnoreCase(algorithm, "with"); + if (indexOfWith > 0) { + algorithm = StrUtil.subSuf(algorithm, indexOfWith + "with".length()); + } + if ("ECDSA".equalsIgnoreCase(algorithm) || "SM2".equalsIgnoreCase(algorithm)) { + algorithm = "EC"; + } + return algorithm; + } + + /** + * 读取密钥库(Java Key Store,JKS) KeyStore文件
+ * KeyStore文件用于数字证书的密钥对保存
+ * see: http://snowolf.iteye.com/blog/391931 + * + * @param in {@link InputStream} 如果想从文件读取.keystore文件,使用 {@link FileUtil#getInputStream(java.io.File)} 读取 + * @param password 密码 + * @return {@link KeyStore} + */ + public static KeyStore readJKSKeyStore(InputStream in, char[] password) { + return readKeyStore(KEY_STORE, in, password); + } + + /** + * 读取KeyStore文件
+ * KeyStore文件用于数字证书的密钥对保存
+ * see: http://snowolf.iteye.com/blog/391931 + * + * @param type 类型 + * @param in {@link InputStream} 如果想从文件读取.keystore文件,使用 {@link FileUtil#getInputStream(java.io.File)} 读取 + * @param password 密码 + * @return {@link KeyStore} + */ + public static KeyStore readKeyStore(String type, InputStream in, char[] password) { + KeyStore keyStore = null; + try { + keyStore = KeyStore.getInstance(type); + keyStore.load(in, password); + } catch (Exception e) { + throw new CryptoException(e); + } + return keyStore; + } + + /** + * 从KeyStore中获取私钥公钥 + * + * @param type 类型 + * @param in {@link InputStream} 如果想从文件读取.keystore文件,使用 {@link FileUtil#getInputStream(java.io.File)} 读取 + * @param password 密码 + * @param alias 别名 + * @return {@link KeyPair} + * @since 4.4.1 + */ + public static KeyPair getKeyPair(String type, InputStream in, char[] password, String alias) { + final KeyStore keyStore = readKeyStore(type, in, password); + return getKeyPair(keyStore, password, alias); + } + + /** + * 从KeyStore中获取私钥公钥 + * + * @param keyStore {@link KeyStore} + * @param password 密码 + * @param alias 别名 + * @return {@link KeyPair} + * @since 4.4.1 + */ + public static KeyPair getKeyPair(KeyStore keyStore, char[] password, String alias) { + PublicKey publicKey; + PrivateKey privateKey; + try { + publicKey = keyStore.getCertificate(alias).getPublicKey(); + privateKey = (PrivateKey) keyStore.getKey(alias, password); + } catch (Exception e) { + throw new CryptoException(e); + } + return new KeyPair(publicKey, privateKey); + } + + /** + * 读取X.509 Certification文件
+ * Certification为证书文件
+ * see: http://snowolf.iteye.com/blog/391931 + * + * @param in {@link InputStream} 如果想从文件读取.cer文件,使用 {@link FileUtil#getInputStream(java.io.File)} 读取 + * @param password 密码 + * @param alias 别名 + * @return {@link KeyStore} + * @since 4.4.1 + */ + public static Certificate readX509Certificate(InputStream in, char[] password, String alias) { + return readCertificate(X509, in, password, alias); + } + + /** + * 读取X.509 Certification文件中的公钥
+ * Certification为证书文件
+ * see: https://www.cnblogs.com/yinliang/p/10115519.html + * + * @param in {@link InputStream} 如果想从文件读取.cer文件,使用 {@link FileUtil#getInputStream(java.io.File)} 读取 + * @return {@link KeyStore} + * @since 4.5.2 + */ + public static PublicKey readPublicKeyFromCert(InputStream in) { + final Certificate certificate = readX509Certificate(in); + if (null != certificate) { + return certificate.getPublicKey(); + } + return null; + } + + /** + * 读取X.509 Certification文件
+ * Certification为证书文件
+ * see: http://snowolf.iteye.com/blog/391931 + * + * @param in {@link InputStream} 如果想从文件读取.cer文件,使用 {@link FileUtil#getInputStream(java.io.File)} 读取 + * @return {@link KeyStore} + * @since 4.4.1 + */ + public static Certificate readX509Certificate(InputStream in) { + return readCertificate(X509, in); + } + + /** + * 读取Certification文件
+ * Certification为证书文件
+ * see: http://snowolf.iteye.com/blog/391931 + * + * @param type 类型,例如X.509 + * @param in {@link InputStream} 如果想从文件读取.cer文件,使用 {@link FileUtil#getInputStream(java.io.File)} 读取 + * @param password 密码 + * @param alias 别名 + * @return {@link KeyStore} + * @since 4.4.1 + */ + public static Certificate readCertificate(String type, InputStream in, char[] password, String alias) { + final KeyStore keyStore = readKeyStore(type, in, password); + try { + return keyStore.getCertificate(alias); + } catch (KeyStoreException e) { + throw new CryptoException(e); + } + } + + /** + * 读取Certification文件
+ * Certification为证书文件
+ * see: http://snowolf.iteye.com/blog/391931 + * + * @param type 类型,例如X.509 + * @param in {@link InputStream} 如果想从文件读取.cer文件,使用 {@link FileUtil#getInputStream(java.io.File)} 读取 + * @return {@link Certificate} + */ + public static Certificate readCertificate(String type, InputStream in) { + try { + return getCertificateFactory(type).generateCertificate(in); + } catch (CertificateException e) { + throw new CryptoException(e); + } + } + + /** + * 获得 Certification + * + * @param keyStore {@link KeyStore} + * @param alias 别名 + * @return {@link Certificate} + */ + public static Certificate getCertificate(KeyStore keyStore, String alias) { + try { + return keyStore.getCertificate(alias); + } catch (Exception e) { + throw new CryptoException(e); + } + } + + /** + * 获取{@link CertificateFactory} + * + * @param type 类型,例如X.509 + * @return {@link KeyPairGenerator} + * @since 4.5.0 + */ + public static CertificateFactory getCertificateFactory(String type) { + final Provider provider = GlobalBouncyCastleProvider.INSTANCE.getProvider(); + + CertificateFactory factory; + try { + factory = (null == provider) ? CertificateFactory.getInstance(type) : CertificateFactory.getInstance(type, provider); + } catch (CertificateException e) { + throw new CryptoException(e); + } + return factory; + } + + /** + * 编码压缩EC公钥(基于BouncyCastle)
+ * 见:https://www.cnblogs.com/xinzhao/p/8963724.html + * + * @param publicKey {@link PublicKey},必须为org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey + * @return 压缩得到的X + * @since 4.4.4 + */ + public static byte[] encodeECPublicKey(PublicKey publicKey) { + return BCUtil.encodeECPublicKey(publicKey); + } + + /** + * 解码恢复EC压缩公钥,支持Base64和Hex编码,(基于BouncyCastle)
+ * 见:https://www.cnblogs.com/xinzhao/p/8963724.html + * + * @param encode 压缩公钥 + * @param curveName EC曲线名 + * @since 4.4.4 + */ + public static PublicKey decodeECPoint(String encode, String curveName) { + return BCUtil.decodeECPoint(encode, curveName); + } + + /** + * 解码恢复EC压缩公钥,支持Base64和Hex编码,(基于BouncyCastle)
+ * 见:https://www.cnblogs.com/xinzhao/p/8963724.html + * + * @param encodeByte 压缩公钥 + * @param curveName EC曲线名 + * @since 4.4.4 + */ + public static PublicKey decodeECPoint(byte[] encodeByte, String curveName) { + return BCUtil.decodeECPoint(encodeByte, curveName); + } +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/Mode.java b/hutool-crypto/src/main/java/cn/hutool/crypto/Mode.java new file mode 100644 index 000000000..2b964e249 --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/Mode.java @@ -0,0 +1,26 @@ +package cn.hutool.crypto; + +/** + * 模式 + * @author Looly + * @see Cipher章节 + * @since 3.0.8 + */ +public enum Mode{ + /** 无模式 */ + NONE, + /** Cipher Block Chaining */ + CBC, + /** Cipher Feedback */ + CFB, + /** A simplification of OFB */ + CTR, + /** Cipher Text Stealing */ + CTS, + /** Electronic Codebook */ + ECB, + /** Output Feedback */ + OFB, + /** Propagating Cipher Block */ + PCBC; +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/Padding.java b/hutool-crypto/src/main/java/cn/hutool/crypto/Padding.java new file mode 100644 index 000000000..cbf5a5757 --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/Padding.java @@ -0,0 +1,18 @@ +package cn.hutool.crypto; + +/** + * 补码方式 + * + * @author Looly + * @see Cipher章节 + * @since 3.0.8 + */ +public enum Padding { + /** 无补码 */ + NoPadding, + ISO10126Padding, + OAEPPadding, + PKCS1Padding, + PKCS5Padding, + SSL3Padding +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/ProviderFactory.java b/hutool-crypto/src/main/java/cn/hutool/crypto/ProviderFactory.java new file mode 100644 index 000000000..11d93115d --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/ProviderFactory.java @@ -0,0 +1,26 @@ +package cn.hutool.crypto; + +import java.security.Provider; + +/** + * Provider对象生产法工厂类 + * + *
+ * 1. 调用{@link #createBouncyCastleProvider()} 用于新建一个org.bouncycastle.jce.provider.BouncyCastleProvider对象
+ * 
+ * + * @author looly + * @since 4.2.1 + */ +public class ProviderFactory { + + /** + * 创建Bouncy Castle 提供者
+ * 如果用户未引入bouncycastle库,则此方法抛出{@link NoClassDefFoundError} 异常 + * + * @return {@link Provider} + */ + public static Provider createBouncyCastleProvider() { + return new org.bouncycastle.jce.provider.BouncyCastleProvider(); + } +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/SecureUtil.java b/hutool-crypto/src/main/java/cn/hutool/crypto/SecureUtil.java new file mode 100644 index 000000000..13b3b720b --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/SecureUtil.java @@ -0,0 +1,1046 @@ +package cn.hutool.crypto; + +import java.io.File; +import java.io.InputStream; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.PublicKey; +import java.security.Security; +import java.security.Signature; +import java.security.cert.Certificate; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.KeySpec; +import java.util.Map; + +import javax.crypto.Cipher; +import javax.crypto.Mac; +import javax.crypto.SecretKey; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.lang.Validator; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.HexUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.asymmetric.AsymmetricAlgorithm; +import cn.hutool.crypto.asymmetric.RSA; +import cn.hutool.crypto.asymmetric.Sign; +import cn.hutool.crypto.asymmetric.SignAlgorithm; +import cn.hutool.crypto.digest.DigestAlgorithm; +import cn.hutool.crypto.digest.Digester; +import cn.hutool.crypto.digest.HMac; +import cn.hutool.crypto.digest.HmacAlgorithm; +import cn.hutool.crypto.digest.MD5; +import cn.hutool.crypto.symmetric.AES; +import cn.hutool.crypto.symmetric.DES; +import cn.hutool.crypto.symmetric.DESede; +import cn.hutool.crypto.symmetric.RC4; +import cn.hutool.crypto.symmetric.SymmetricCrypto; + +/** + * 安全相关工具类
+ * 加密分为三种:
+ * 1、对称加密(symmetric),例如:AES、DES等
+ * 2、非对称加密(asymmetric),例如:RSA、DSA等
+ * 3、摘要加密(digest),例如:MD5、SHA-1、SHA-256、HMAC等
+ * + * @author xiaoleilu, Gsealy + * + */ +public final class SecureUtil { + + /** + * 默认密钥字节数 + * + *
+	 * RSA/DSA
+	 * Default Keysize 1024
+	 * Keysize must be a multiple of 64, ranging from 512 to 1024 (inclusive).
+	 * 
+ */ + public static final int DEFAULT_KEY_SIZE = KeyUtil.DEFAULT_KEY_SIZE; + + /** + * 生成 {@link SecretKey},仅用于对称加密和摘要算法密钥生成 + * + * @param algorithm 算法,支持PBE算法 + * @return {@link SecretKey} + */ + public static SecretKey generateKey(String algorithm) { + return KeyUtil.generateKey(algorithm); + } + + /** + * 生成 {@link SecretKey},仅用于对称加密和摘要算法密钥生成 + * + * @param algorithm 算法,支持PBE算法 + * @param keySize 密钥长度 + * @return {@link SecretKey} + * @since 3.1.2 + */ + public static SecretKey generateKey(String algorithm, int keySize) { + return KeyUtil.generateKey(algorithm, keySize); + } + + /** + * 生成 {@link SecretKey},仅用于对称加密和摘要算法密钥生成 + * + * @param algorithm 算法 + * @param key 密钥,如果为{@code null} 自动生成随机密钥 + * @return {@link SecretKey} + */ + public static SecretKey generateKey(String algorithm, byte[] key) { + return KeyUtil.generateKey(algorithm, key); + } + + /** + * 生成 {@link SecretKey} + * + * @param algorithm DES算法,包括DES、DESede等 + * @param key 密钥 + * @return {@link SecretKey} + */ + public static SecretKey generateDESKey(String algorithm, byte[] key) { + return KeyUtil.generateDESKey(algorithm, key); + } + + /** + * 生成PBE {@link SecretKey} + * + * @param algorithm PBE算法,包括:PBEWithMD5AndDES、PBEWithSHA1AndDESede、PBEWithSHA1AndRC2_40等 + * @param key 密钥 + * @return {@link SecretKey} + */ + public static SecretKey generatePBEKey(String algorithm, char[] key) { + return KeyUtil.generatePBEKey(algorithm, key); + } + + /** + * 生成 {@link SecretKey},仅用于对称加密和摘要算法 + * + * @param algorithm 算法 + * @param keySpec {@link KeySpec} + * @return {@link SecretKey} + */ + public static SecretKey generateKey(String algorithm, KeySpec keySpec) { + return KeyUtil.generateKey(algorithm, keySpec); + } + + /** + * 生成私钥,仅用于非对称加密
+ * 算法见:https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyFactory + * + * @param algorithm 算法 + * @param key 密钥 + * @return 私钥 {@link PrivateKey} + */ + public static PrivateKey generatePrivateKey(String algorithm, byte[] key) { + return KeyUtil.generatePrivateKey(algorithm, key); + } + + /** + * 生成私钥,仅用于非对称加密
+ * 算法见:https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyFactory + * + * @param algorithm 算法 + * @param keySpec {@link KeySpec} + * @return 私钥 {@link PrivateKey} + * @since 3.1.1 + */ + public static PrivateKey generatePrivateKey(String algorithm, KeySpec keySpec) { + return KeyUtil.generatePrivateKey(algorithm, keySpec); + } + + /** + * 生成私钥,仅用于非对称加密 + * + * @param keyStore {@link KeyStore} + * @param alias 别名 + * @param password 密码 + * @return 私钥 {@link PrivateKey} + */ + public static PrivateKey generatePrivateKey(KeyStore keyStore, String alias, char[] password) { + return KeyUtil.generatePrivateKey(keyStore, alias, password); + } + + /** + * 生成公钥,仅用于非对称加密
+ * 算法见:https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyFactory + * + * @param algorithm 算法 + * @param key 密钥 + * @return 公钥 {@link PublicKey} + */ + public static PublicKey generatePublicKey(String algorithm, byte[] key) { + return KeyUtil.generatePublicKey(algorithm, key); + } + + /** + * 生成公钥,仅用于非对称加密
+ * 算法见:https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyFactory + * + * @param algorithm 算法 + * @param keySpec {@link KeySpec} + * @return 公钥 {@link PublicKey} + * @since 3.1.1 + */ + public static PublicKey generatePublicKey(String algorithm, KeySpec keySpec) { + return KeyUtil.generatePublicKey(algorithm, keySpec); + } + + /** + * 生成用于非对称加密的公钥和私钥,仅用于非对称加密
+ * 密钥对生成算法见:https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyPairGenerator + * + * @param algorithm 非对称加密算法 + * @return {@link KeyPair} + */ + public static KeyPair generateKeyPair(String algorithm) { + return KeyUtil.generateKeyPair(algorithm); + } + + /** + * 生成用于非对称加密的公钥和私钥
+ * 密钥对生成算法见:https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyPairGenerator + * + * @param algorithm 非对称加密算法 + * @param keySize 密钥模(modulus )长度 + * @return {@link KeyPair} + */ + public static KeyPair generateKeyPair(String algorithm, int keySize) { + return KeyUtil.generateKeyPair(algorithm, keySize); + } + + /** + * 生成用于非对称加密的公钥和私钥
+ * 密钥对生成算法见:https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyPairGenerator + * + * @param algorithm 非对称加密算法 + * @param keySize 密钥模(modulus )长度 + * @param seed 种子 + * @return {@link KeyPair} + */ + public static KeyPair generateKeyPair(String algorithm, int keySize, byte[] seed) { + return KeyUtil.generateKeyPair(algorithm, keySize, seed); + } + + /** + * 生成用于非对称加密的公钥和私钥
+ * 密钥对生成算法见:https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyPairGenerator + * + * @param algorithm 非对称加密算法 + * @param params {@link AlgorithmParameterSpec} + * @return {@link KeyPair} + * @since 4.3.3 + */ + public static KeyPair generateKeyPair(String algorithm, AlgorithmParameterSpec params) { + return KeyUtil.generateKeyPair(algorithm, params); + } + + /** + * 生成用于非对称加密的公钥和私钥
+ * 密钥对生成算法见:https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyPairGenerator + * + * @param algorithm 非对称加密算法 + * @param seed 种子 + * @param params {@link AlgorithmParameterSpec} + * @return {@link KeyPair} + * @since 4.3.3 + */ + public static KeyPair generateKeyPair(String algorithm, byte[] seed, AlgorithmParameterSpec params) { + return KeyUtil.generateKeyPair(algorithm, seed, params); + } + + /** + * 获取用于密钥生成的算法
+ * 获取XXXwithXXX算法的后半部分算法,如果为ECDSA或SM2,返回算法为EC + * + * @param algorithm XXXwithXXX算法 + * @return 算法 + */ + public static String getAlgorithmAfterWith(String algorithm) { + return KeyUtil.getAlgorithmAfterWith(algorithm); + } + + /** + * 生成算法,格式为XXXwithXXX + * + * @param asymmetricAlgorithm 非对称算法 + * @param digestAlgorithm 摘要算法 + * @return 算法 + * @since 4.4.1 + */ + public static String generateAlgorithm(AsymmetricAlgorithm asymmetricAlgorithm, DigestAlgorithm digestAlgorithm) { + final String digestPart = (null == digestAlgorithm) ? "NONE" : digestAlgorithm.name(); + return StrUtil.format("{}with{}", digestPart, asymmetricAlgorithm.getValue()); + } + + /** + * 生成签名对象,仅用于非对称加密 + * + * @param asymmetricAlgorithm {@link AsymmetricAlgorithm} 非对称加密算法 + * @param digestAlgorithm {@link DigestAlgorithm} 摘要算法 + * @return {@link Signature} + */ + public static Signature generateSignature(AsymmetricAlgorithm asymmetricAlgorithm, DigestAlgorithm digestAlgorithm) { + try { + return Signature.getInstance(generateAlgorithm(asymmetricAlgorithm, digestAlgorithm)); + } catch (NoSuchAlgorithmException e) { + throw new CryptoException(e); + } + } + + /** + * 读取密钥库(Java Key Store,JKS) KeyStore文件
+ * KeyStore文件用于数字证书的密钥对保存
+ * see: http://snowolf.iteye.com/blog/391931 + * + * @param in {@link InputStream} 如果想从文件读取.keystore文件,使用 {@link FileUtil#getInputStream(java.io.File)} 读取 + * @param password 密码 + * @return {@link KeyStore} + */ + public static KeyStore readJKSKeyStore(InputStream in, char[] password) { + return KeyUtil.readJKSKeyStore(in, password); + } + + /** + * 读取KeyStore文件
+ * KeyStore文件用于数字证书的密钥对保存
+ * see: http://snowolf.iteye.com/blog/391931 + * + * @param type 类型 + * @param in {@link InputStream} 如果想从文件读取.keystore文件,使用 {@link FileUtil#getInputStream(java.io.File)} 读取 + * @param password 密码 + * @return {@link KeyStore} + */ + public static KeyStore readKeyStore(String type, InputStream in, char[] password) { + return KeyUtil.readKeyStore(type, in, password); + } + + /** + * 读取X.509 Certification文件
+ * Certification为证书文件
+ * see: http://snowolf.iteye.com/blog/391931 + * + * @param in {@link InputStream} 如果想从文件读取.cer文件,使用 {@link FileUtil#getInputStream(java.io.File)} 读取 + * @param password 密码 + * @param alias 别名 + * @return {@link KeyStore} + * @since 4.4.1 + */ + public static Certificate readX509Certificate(InputStream in, char[] password, String alias) { + return KeyUtil.readX509Certificate(in, password, alias); + } + + /** + * 读取X.509 Certification文件
+ * Certification为证书文件
+ * see: http://snowolf.iteye.com/blog/391931 + * + * @param in {@link InputStream} 如果想从文件读取.cer文件,使用 {@link FileUtil#getInputStream(java.io.File)} 读取 + * @return {@link KeyStore} + * @since 4.4.1 + */ + public static Certificate readX509Certificate(InputStream in) { + return KeyUtil.readX509Certificate(in); + } + + /** + * 读取Certification文件
+ * Certification为证书文件
+ * see: http://snowolf.iteye.com/blog/391931 + * + * @param type 类型,例如X.509 + * @param in {@link InputStream} 如果想从文件读取.cer文件,使用 {@link FileUtil#getInputStream(java.io.File)} 读取 + * @param password 密码 + * @param alias 别名 + * @return {@link KeyStore} + * @since 4.4.1 + */ + public static Certificate readCertificate(String type, InputStream in, char[] password, String alias) { + return KeyUtil.readCertificate(type, in, password, alias); + } + + /** + * 读取Certification文件
+ * Certification为证书文件
+ * see: http://snowolf.iteye.com/blog/391931 + * + * @param type 类型,例如X.509 + * @param in {@link InputStream} 如果想从文件读取.cer文件,使用 {@link FileUtil#getInputStream(java.io.File)} 读取 + * @return {@link Certificate} + */ + public static Certificate readCertificate(String type, InputStream in) { + return KeyUtil.readCertificate(type, in); + } + + /** + * 获得 Certification + * + * @param keyStore {@link KeyStore} + * @param alias 别名 + * @return {@link Certificate} + */ + public static Certificate getCertificate(KeyStore keyStore, String alias) { + return KeyUtil.getCertificate(keyStore, alias); + } + + // ------------------------------------------------------------------- 对称加密算法 + + /** + * AES加密,生成随机KEY。注意解密时必须使用相同 {@link AES}对象或者使用相同KEY
+ * 例: + * + *
+	 * AES加密:aes().encrypt(data)
+	 * AES解密:aes().decrypt(data)
+	 * 
+ * + * @return {@link AES} + */ + public static AES aes() { + return new AES(); + } + + /** + * AES加密
+ * 例: + * + *
+	 * AES加密:aes(key).encrypt(data)
+	 * AES解密:aes(key).decrypt(data)
+	 * 
+ * + * @param key 密钥 + * @return {@link SymmetricCrypto} + */ + public static AES aes(byte[] key) { + return new AES(key); + } + + /** + * DES加密,生成随机KEY。注意解密时必须使用相同 {@link DES}对象或者使用相同KEY
+ * 例: + * + *
+	 * DES加密:des().encrypt(data)
+	 * DES解密:des().decrypt(data)
+	 * 
+ * + * @return {@link DES} + */ + public static DES des() { + return new DES(); + } + + /** + * DES加密
+ * 例: + * + *
+	 * DES加密:des(key).encrypt(data)
+	 * DES解密:des(key).decrypt(data)
+	 * 
+ * + * @param key 密钥 + * @return {@link DES} + */ + public static DES des(byte[] key) { + return new DES(key); + } + + /** + * DESede加密(又名3DES、TripleDES),生成随机KEY。注意解密时必须使用相同 {@link DESede}对象或者使用相同KEY
+ * Java中默认实现为:DESede/ECB/PKCS5Padding
+ * 例: + * + *
+	 * DESede加密:desede().encrypt(data)
+	 * DESede解密:desede().decrypt(data)
+	 * 
+ * + * @return {@link DESede} + * @since 3.3.0 + */ + public static DESede desede() { + return new DESede(); + } + + /** + * DESede加密(又名3DES、TripleDES)
+ * Java中默认实现为:DESede/ECB/PKCS5Padding
+ * 例: + * + *
+	 * DESede加密:desede(key).encrypt(data)
+	 * DESede解密:desede(key).decrypt(data)
+	 * 
+ * + * @param key 密钥 + * @return {@link DESede} + * @since 3.3.0 + */ + public static DESede desede(byte[] key) { + return new DESede(key); + } + + // ------------------------------------------------------------------- 摘要算法 + /** + * MD5加密
+ * 例: + * + *
+	 * MD5加密:md5().digest(data)
+	 * MD5加密并转为16进制字符串:md5().digestHex(data)
+	 * 
+ * + * @return {@link Digester} + */ + public static MD5 md5() { + return new MD5(); + } + + /** + * MD5加密,生成16进制MD5字符串
+ * + * @param data 数据 + * @return MD5字符串 + */ + public static String md5(String data) { + return new MD5().digestHex(data); + } + + /** + * MD5加密,生成16进制MD5字符串
+ * + * @param data 数据 + * @return MD5字符串 + */ + public static String md5(InputStream data) { + return new MD5().digestHex(data); + } + + /** + * MD5加密文件,生成16进制MD5字符串
+ * + * @param dataFile 被加密文件 + * @return MD5字符串 + */ + public static String md5(File dataFile) { + return new MD5().digestHex(dataFile); + } + + /** + * SHA1加密
+ * 例:
+ * SHA1加密:sha1().digest(data)
+ * SHA1加密并转为16进制字符串:sha1().digestHex(data)
+ * + * @return {@link Digester} + */ + public static Digester sha1() { + return new Digester(DigestAlgorithm.SHA1); + } + + /** + * SHA1加密,生成16进制SHA1字符串
+ * + * @param data 数据 + * @return SHA1字符串 + */ + public static String sha1(String data) { + return new Digester(DigestAlgorithm.SHA1).digestHex(data); + } + + /** + * SHA1加密,生成16进制SHA1字符串
+ * + * @param data 数据 + * @return SHA1字符串 + */ + public static String sha1(InputStream data) { + return new Digester(DigestAlgorithm.SHA1).digestHex(data); + } + + /** + * SHA1加密文件,生成16进制SHA1字符串
+ * + * @param dataFile 被加密文件 + * @return SHA1字符串 + */ + public static String sha1(File dataFile) { + return new Digester(DigestAlgorithm.SHA1).digestHex(dataFile); + } + + /** + * SHA256加密
+ * 例:
+ * SHA256加密:sha256().digest(data)
+ * SHA256加密并转为16进制字符串:sha256().digestHex(data)
+ * + * @return {@link Digester} + * @since 4.3.2 + */ + public static Digester sha256() { + return new Digester(DigestAlgorithm.SHA256); + } + + /** + * SHA256加密,生成16进制SHA256字符串
+ * + * @param data 数据 + * @return SHA256字符串 + * @since 4.3.2 + */ + public static String sha256(String data) { + return new Digester(DigestAlgorithm.SHA256).digestHex(data); + } + + /** + * SHA256加密,生成16进制SHA256字符串
+ * + * @param data 数据 + * @return SHA1字符串 + * @since 4.3.2 + */ + public static String sha256(InputStream data) { + return new Digester(DigestAlgorithm.SHA256).digestHex(data); + } + + /** + * SHA256加密文件,生成16进制SHA256字符串
+ * + * @param dataFile 被加密文件 + * @return SHA256字符串 + * @since 4.3.2 + */ + public static String sha256(File dataFile) { + return new Digester(DigestAlgorithm.SHA256).digestHex(dataFile); + } + + /** + * 创建HMac对象,调用digest方法可获得hmac值 + * + * @param algorithm {@link HmacAlgorithm} + * @param key 密钥,如果为null生成随机密钥 + * @return {@link HMac} + * @since 3.3.0 + */ + public static HMac hmac(HmacAlgorithm algorithm, String key) { + return new HMac(algorithm, StrUtil.utf8Bytes(key)); + } + + /** + * 创建HMac对象,调用digest方法可获得hmac值 + * + * @param algorithm {@link HmacAlgorithm} + * @param key 密钥,如果为null生成随机密钥 + * @return {@link HMac} + * @since 3.0.3 + */ + public static HMac hmac(HmacAlgorithm algorithm, byte[] key) { + return new HMac(algorithm, key); + } + + /** + * 创建HMac对象,调用digest方法可获得hmac值 + * + * @param algorithm {@link HmacAlgorithm} + * @param key 密钥{@link SecretKey},如果为null生成随机密钥 + * @return {@link HMac} + * @since 3.0.3 + */ + public static HMac hmac(HmacAlgorithm algorithm, SecretKey key) { + return new HMac(algorithm, key); + } + + /** + * HmacMD5加密器
+ * 例:
+ * HmacMD5加密:hmacMd5(key).digest(data)
+ * HmacMD5加密并转为16进制字符串:hmacMd5(key).digestHex(data)
+ * + * @param key 加密密钥,如果为null生成随机密钥 + * @return {@link HMac} + * @since 3.3.0 + */ + public static HMac hmacMd5(String key) { + return hmacMd5(StrUtil.utf8Bytes(key)); + } + + /** + * HmacMD5加密器
+ * 例:
+ * HmacMD5加密:hmacMd5(key).digest(data)
+ * HmacMD5加密并转为16进制字符串:hmacMd5(key).digestHex(data)
+ * + * @param key 加密密钥,如果为null生成随机密钥 + * @return {@link HMac} + */ + public static HMac hmacMd5(byte[] key) { + return new HMac(HmacAlgorithm.HmacMD5, key); + } + + /** + * HmacMD5加密器,生成随机KEY
+ * 例:
+ * HmacMD5加密:hmacMd5().digest(data)
+ * HmacMD5加密并转为16进制字符串:hmacMd5().digestHex(data)
+ * + * @return {@link HMac} + */ + public static HMac hmacMd5() { + return new HMac(HmacAlgorithm.HmacMD5); + } + + /** + * HmacSHA1加密器
+ * 例:
+ * HmacSHA1加密:hmacSha1(key).digest(data)
+ * HmacSHA1加密并转为16进制字符串:hmacSha1(key).digestHex(data)
+ * + * @param key 加密密钥,如果为null生成随机密钥 + * @return {@link HMac} + * @since 3.3.0 + */ + public static HMac hmacSha1(String key) { + return hmacSha1(StrUtil.utf8Bytes(key)); + } + + /** + * HmacSHA1加密器
+ * 例:
+ * HmacSHA1加密:hmacSha1(key).digest(data)
+ * HmacSHA1加密并转为16进制字符串:hmacSha1(key).digestHex(data)
+ * + * @param key 加密密钥,如果为null生成随机密钥 + * @return {@link HMac} + */ + public static HMac hmacSha1(byte[] key) { + return new HMac(HmacAlgorithm.HmacSHA1, key); + } + + /** + * HmacSHA1加密器,生成随机KEY
+ * 例:
+ * HmacSHA1加密:hmacSha1().digest(data)
+ * HmacSHA1加密并转为16进制字符串:hmacSha1().digestHex(data)
+ * + * @return {@link HMac} + */ + public static HMac hmacSha1() { + return new HMac(HmacAlgorithm.HmacSHA1); + } + + // ------------------------------------------------------------------- 非称加密算法 + + /** + * 创建RSA算法对象
+ * 生成新的私钥公钥对 + * + * @return {@link RSA} + * @since 3.0.5 + */ + public static RSA rsa() { + return new RSA(); + } + + /** + * 创建RSA算法对象
+ * 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做加密或者解密 + * + * @param privateKeyBase64 私钥Base64 + * @param publicKeyBase64 公钥Base64 + * @return {@link RSA} + * @since 3.0.5 + */ + public static RSA rsa(String privateKeyBase64, String publicKeyBase64) { + return new RSA(privateKeyBase64, publicKeyBase64); + } + + /** + * 创建RSA算法对象
+ * 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做加密或者解密 + * + * @param privateKey 私钥 + * @param publicKey 公钥 + * @return {@link RSA} + * @since 3.0.5 + */ + public static RSA rsa(byte[] privateKey, byte[] publicKey) { + return new RSA(privateKey, publicKey); + } + + /** + * 创建签名算法对象
+ * 生成新的私钥公钥对 + * + * @param algorithm 签名算法 + * @return {@link Sign} + * @since 3.3.0 + */ + public static Sign sign(SignAlgorithm algorithm) { + return new Sign(algorithm); + } + + /** + * 创建签名算法对象
+ * 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做签名或验证 + * + * @param algorithm 签名算法 + * @param privateKeyBase64 私钥Base64 + * @param publicKeyBase64 公钥Base64 + * @return {@link Sign} + * @since 3.3.0 + */ + public static Sign sign(SignAlgorithm algorithm, String privateKeyBase64, String publicKeyBase64) { + return new Sign(algorithm, privateKeyBase64, publicKeyBase64); + } + + /** + * 创建Sign算法对象
+ * 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做签名或验证 + * + * @param privateKey 私钥 + * @param publicKey 公钥 + * @return {@link Sign} + * @since 3.3.0 + */ + public static Sign sign(SignAlgorithm algorithm, byte[] privateKey, byte[] publicKey) { + return new Sign(algorithm, privateKey, publicKey); + } + + /** + * 对参数做签名
+ * 参数签名为对Map参数按照key的顺序排序后拼接为字符串,然后根据提供的签名算法生成签名字符串
+ * 拼接后的字符串键值对之间无符号,键值对之间无符号,忽略null值 + * + * @param crypto 对称加密算法 + * @param params 参数 + * @return 签名 + * @since 4.0.1 + */ + public static String signParams(SymmetricCrypto crypto, Map params) { + return signParams(crypto, params, StrUtil.EMPTY, StrUtil.EMPTY, true); + } + + /** + * 对参数做签名
+ * 参数签名为对Map参数按照key的顺序排序后拼接为字符串,然后根据提供的签名算法生成签名字符串 + * + * @param crypto 对称加密算法 + * @param params 参数 + * @param separator entry之间的连接符 + * @param keyValueSeparator kv之间的连接符 + * @param isIgnoreNull 是否忽略null的键和值 + * @return 签名 + * @since 4.0.1 + */ + public static String signParams(SymmetricCrypto crypto, Map params, String separator, String keyValueSeparator, boolean isIgnoreNull) { + if (MapUtil.isEmpty(params)) { + return null; + } + String paramsStr = MapUtil.join(MapUtil.sort(params), separator, keyValueSeparator, isIgnoreNull); + return crypto.encryptHex(paramsStr); + } + + /** + * 对参数做md5签名
+ * 参数签名为对Map参数按照key的顺序排序后拼接为字符串,然后根据提供的签名算法生成签名字符串
+ * 拼接后的字符串键值对之间无符号,键值对之间无符号,忽略null值 + * + * @param params 参数 + * @return 签名 + * @since 4.0.1 + */ + public static String signParamsMd5(Map params) { + return signParams(DigestAlgorithm.MD5, params); + } + + /** + * 对参数做Sha1签名
+ * 参数签名为对Map参数按照key的顺序排序后拼接为字符串,然后根据提供的签名算法生成签名字符串
+ * 拼接后的字符串键值对之间无符号,键值对之间无符号,忽略null值 + * + * @param params 参数 + * @return 签名 + * @since 4.0.8 + */ + public static String signParamsSha1(Map params) { + return signParams(DigestAlgorithm.SHA1, params); + } + + /** + * 对参数做Sha256签名
+ * 参数签名为对Map参数按照key的顺序排序后拼接为字符串,然后根据提供的签名算法生成签名字符串
+ * 拼接后的字符串键值对之间无符号,键值对之间无符号,忽略null值 + * + * @param params 参数 + * @return 签名 + * @since 4.0.1 + */ + public static String signParamsSha256(Map params) { + return signParams(DigestAlgorithm.SHA256, params); + } + + /** + * 对参数做签名
+ * 参数签名为对Map参数按照key的顺序排序后拼接为字符串,然后根据提供的签名算法生成签名字符串
+ * 拼接后的字符串键值对之间无符号,键值对之间无符号,忽略null值 + * + * @param digestAlgorithm 摘要算法 + * @param params 参数 + * @return 签名 + * @since 4.0.1 + */ + public static String signParams(DigestAlgorithm digestAlgorithm, Map params) { + return signParams(digestAlgorithm, params, StrUtil.EMPTY, StrUtil.EMPTY, true); + } + + /** + * 对参数做签名
+ * 参数签名为对Map参数按照key的顺序排序后拼接为字符串,然后根据提供的签名算法生成签名字符串 + * + * @param digestAlgorithm 摘要算法 + * @param params 参数 + * @param separator entry之间的连接符 + * @param keyValueSeparator kv之间的连接符 + * @param isIgnoreNull 是否忽略null的键和值 + * @return 签名 + * @since 4.0.1 + */ + public static String signParams(DigestAlgorithm digestAlgorithm, Map params, String separator, String keyValueSeparator, boolean isIgnoreNull) { + if (MapUtil.isEmpty(params)) { + return null; + } + final String paramsStr = MapUtil.join(MapUtil.sort(params), separator, keyValueSeparator, isIgnoreNull); + return new Digester(digestAlgorithm).digestHex(paramsStr); + } + + // ------------------------------------------------------------------- UUID + /** + * 简化的UUID,去掉了横线 + * + * @return 简化的UUID,去掉了横线 + * @deprecated 请使用 {@link IdUtil#simpleUUID()} + */ + @Deprecated + public static String simpleUUID() { + return IdUtil.simpleUUID(); + } + + /** + * 增加加密解密的算法提供者,默认优先使用,例如: + * + *
+	 * addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
+	 * 
+ * + * @param provider 算法提供者 + * @since 4.1.22 + */ + public static void addProvider(Provider provider) { + Security.insertProviderAt(provider, 0); + } + + /** + * 解码字符串密钥,可支持的编码如下: + * + *
+	 * 1. Hex(16进制)编码
+	 * 1. Base64编码
+	 * 
+ * + * @param key 被解码的密钥字符串 + * @return 密钥 + * @since 4.3.3 + */ + public static byte[] decode(String key) { + return Validator.isHex(key) ? HexUtil.decodeHex(key) : Base64.decode(key); + } + + /** + * 创建{@link Cipher} + * + * @param algorithm 算法 + * @since 4.5.2 + */ + public static Cipher createCipher(String algorithm) { + final Provider provider = GlobalBouncyCastleProvider.INSTANCE.getProvider(); + + Cipher cipher; + try { + cipher = (null == provider) ? Cipher.getInstance(algorithm) : Cipher.getInstance(algorithm, provider); + } catch (Exception e) { + throw new CryptoException(e); + } + + return cipher; + } + + /** + * 创建{@link MessageDigest} + * + * @param algorithm 算法 + * @since 4.5.2 + */ + public static MessageDigest createMessageDigest(String algorithm) { + final Provider provider = GlobalBouncyCastleProvider.INSTANCE.getProvider(); + + MessageDigest messageDigest; + try { + messageDigest = (null == provider) ? MessageDigest.getInstance(algorithm) : MessageDigest.getInstance(algorithm, provider); + } catch (NoSuchAlgorithmException e) { + throw new CryptoException(e); + } + + return messageDigest; + } + + /** + * 创建{@link Mac} + * + * @param algorithm 算法 + * @since 4.5.13 + */ + public static Mac createMac(String algorithm) { + final Provider provider = GlobalBouncyCastleProvider.INSTANCE.getProvider(); + + Mac mac; + try { + mac = (null == provider) ? Mac.getInstance(algorithm) : Mac.getInstance(algorithm, provider); + } catch (NoSuchAlgorithmException e) { + throw new CryptoException(e); + } + + return mac; + } + + /** + * RC4算法 + * + * @param key 密钥 + * @return {@link RC4} + */ + public static RC4 rc4(String key) { + return new RC4(key); + } + + /** + * 强制关闭Bouncy Castle库的使用,全局有效 + * + * @since 4.5.2 + */ + public static void disableBouncyCastle() { + GlobalBouncyCastleProvider.setUseBouncyCastle(false); + } +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/SmUtil.java b/hutool-crypto/src/main/java/cn/hutool/crypto/SmUtil.java new file mode 100644 index 000000000..09307e0db --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/SmUtil.java @@ -0,0 +1,275 @@ +package cn.hutool.crypto; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; + +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.crypto.digests.SM3Digest; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.util.Arrays; +import org.bouncycastle.util.encoders.Hex; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.crypto.asymmetric.SM2; +import cn.hutool.crypto.digest.Digester; +import cn.hutool.crypto.digest.HMac; +import cn.hutool.crypto.digest.HmacAlgorithm; +import cn.hutool.crypto.digest.mac.BCHMacEngine; +import cn.hutool.crypto.digest.mac.MacEngine; +import cn.hutool.crypto.symmetric.SymmetricCrypto; + +/** + * SM国密算法工具类
+ * 此工具类依赖org.bouncycastle:bcpkix-jdk15on + * + * @author looly + * @since 4.3.2 + */ +public class SmUtil { + + private final static int RS_LEN = 32; + + private static String SM3 = "SM3"; + private static String SM4 = "SM4"; + + /** + * 创建SM2算法对象
+ * 生成新的私钥公钥对 + * + * @return {@link SM2} + */ + public static SM2 sm2() { + return new SM2(); + } + + /** + * 创建SM2算法对象
+ * 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做加密或者解密 + * + * @param privateKeyStr 私钥Hex或Base64表示 + * @param publicKeyStr 公钥Hex或Base64表示 + * @return {@link SM2} + */ + public static SM2 sm2(String privateKeyStr, String publicKeyStr) { + return new SM2(privateKeyStr, publicKeyStr); + } + + /** + * 创建SM2算法对象
+ * 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做加密或者解密 + * + * @param privateKey 私钥 + * @param publicKey 公钥 + * @return {@link SM2} + */ + public static SM2 sm2(byte[] privateKey, byte[] publicKey) { + return new SM2(privateKey, publicKey); + } + + /** + * SM3加密
+ * 例:
+ * SM3加密:sm3().digest(data)
+ * SM3加密并转为16进制字符串:sm3().digestHex(data)
+ * + * @return {@link Digester} + */ + public static Digester sm3() { + return new Digester(SM3); + } + + /** + * SM3加密,生成16进制SM3字符串
+ * + * @param data 数据 + * @return SM3字符串 + */ + public static String sm3(String data) { + return new Digester(SM3).digestHex(data); + } + + /** + * SM3加密,生成16进制SM3字符串
+ * + * @param data 数据 + * @return SM3字符串 + */ + public static String sm3(InputStream data) { + return new Digester(SM3).digestHex(data); + } + + /** + * SM3加密文件,生成16进制SM3字符串
+ * + * @param dataFile 被加密文件 + * @return SM3字符串 + */ + public static String sm3(File dataFile) { + return new Digester(SM3).digestHex(dataFile); + } + + /** + * SM4加密,生成随机KEY。注意解密时必须使用相同 {@link SymmetricCrypto}对象或者使用相同KEY
+ * 例: + * + *
+	 * SM4加密:sm4().encrypt(data)
+	 * SM4解密:sm4().decrypt(data)
+	 * 
+ * + * @return {@link SymmetricCrypto} + */ + public static SymmetricCrypto sm4() { + return new SymmetricCrypto(SM4); + } + + /** + * SM4加密
+ * 例: + * + *
+	 * SM4加密:sm4(key).encrypt(data)
+	 * SM4解密:sm4(key).decrypt(data)
+	 * 
+ * + * @param key 密钥 + * @return {@link SymmetricCrypto} + */ + public static SymmetricCrypto sm4(byte[] key) { + return new SymmetricCrypto(SM4, key); + } + + /** + * bc加解密使用旧标c1||c2||c3,此方法在加密后调用,将结果转化为c1||c3||c2 + * + * @param c1c2c3 加密后的bytes,顺序为C1C2C3 + * @param ecDomainParameters {@link ECDomainParameters} + * @return 加密后的bytes,顺序为C1C3C2 + */ + public static byte[] changeC1C2C3ToC1C3C2(byte[] c1c2c3, ECDomainParameters ecDomainParameters) { + // sm2p256v1的这个固定65。可看GMNamedCurves、ECCurve代码。 + final int c1Len = (ecDomainParameters.getCurve().getFieldSize() + 7) / 8 * 2 + 1; + final int c3Len = 32; // new SM3Digest().getDigestSize(); + byte[] result = new byte[c1c2c3.length]; + System.arraycopy(c1c2c3, 0, result, 0, c1Len); // c1 + System.arraycopy(c1c2c3, c1c2c3.length - c3Len, result, c1Len, c3Len); // c3 + System.arraycopy(c1c2c3, c1Len, result, c1Len + c3Len, c1c2c3.length - c1Len - c3Len); // c2 + return result; + } + + /** + * bc加解密使用旧标c1||c3||c2,此方法在解密前调用,将密文转化为c1||c2||c3再去解密 + * + * @param c1c3c2 加密后的bytes,顺序为C1C3C2 + * @param ecDomainParameters {@link ECDomainParameters} + * @return c1c2c3 加密后的bytes,顺序为C1C2C3 + */ + public static byte[] changeC1C3C2ToC1C2C3(byte[] c1c3c2, ECDomainParameters ecDomainParameters) { + // sm2p256v1的这个固定65。可看GMNamedCurves、ECCurve代码。 + final int c1Len = (ecDomainParameters.getCurve().getFieldSize() + 7) / 8 * 2 + 1; + final int c3Len = 32; // new SM3Digest().getDigestSize(); + byte[] result = new byte[c1c3c2.length]; + System.arraycopy(c1c3c2, 0, result, 0, c1Len); // c1: 0->65 + System.arraycopy(c1c3c2, c1Len + c3Len, result, c1Len, c1c3c2.length - c1Len - c3Len); // c2 + System.arraycopy(c1c3c2, c1Len, result, c1c3c2.length - c3Len, c3Len); // c3 + return result; + } + + /** + * BC的SM3withSM2签名得到的结果的rs是asn1格式的,这个方法转化成直接拼接r||s
+ * 来自:https://blog.csdn.net/pridas/article/details/86118774 + * + * @param rsDer rs in asn1 format + * @return sign result in plain byte array + * @since 4.5.0 + */ + public static byte[] rsAsn1ToPlain(byte[] rsDer) { + ASN1Sequence seq = ASN1Sequence.getInstance(rsDer); + byte[] r = bigIntToFixexLengthBytes(ASN1Integer.getInstance(seq.getObjectAt(0)).getValue()); + byte[] s = bigIntToFixexLengthBytes(ASN1Integer.getInstance(seq.getObjectAt(1)).getValue()); + byte[] result = new byte[RS_LEN * 2]; + System.arraycopy(r, 0, result, 0, r.length); + System.arraycopy(s, 0, result, RS_LEN, s.length); + return result; + } + + /** + * BC的SM3withSM2验签需要的rs是asn1格式的,这个方法将直接拼接r||s的字节数组转化成asn1格式
+ * 来自:https://blog.csdn.net/pridas/article/details/86118774 + * + * @param sign in plain byte array + * @return rs result in asn1 format + * @since 4.5.0 + */ + public static byte[] rsPlainToAsn1(byte[] sign) { + if (sign.length != RS_LEN * 2) { + throw new CryptoException("err rs. "); + } + BigInteger r = new BigInteger(1, Arrays.copyOfRange(sign, 0, RS_LEN)); + BigInteger s = new BigInteger(1, Arrays.copyOfRange(sign, RS_LEN, RS_LEN * 2)); + ASN1EncodableVector v = new ASN1EncodableVector(); + v.add(new ASN1Integer(r)); + v.add(new ASN1Integer(s)); + try { + return new DERSequence(v).getEncoded("DER"); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 创建HmacSM3算法的{@link MacEngine} + * + * @param key 密钥 + * @return {@link MacEngine} + * @since 4.5.13 + */ + public static MacEngine createHmacSm3Engine(byte[] key) { + return new BCHMacEngine(new SM3Digest(), key); + } + + /** + * HmacSM3算法实现 + * + * @param key 密钥 + * @return {@link HMac} 对象,调用digestXXX即可 + * @since 4.5.13 + */ + public static HMac hmacSm3(byte[] key) { + return new HMac(HmacAlgorithm.HmacSM3, key); + } + + // -------------------------------------------------------------------------------------------------------- Private method start + /** + * BigInteger转固定长度bytes + * + * @param rOrS {@link BigInteger} + * @return 固定长度bytes + * @since 4.5.0 + */ + private static byte[] bigIntToFixexLengthBytes(BigInteger rOrS) { + // for sm2p256v1, n is 00fffffffeffffffffffffffffffffffff7203df6b21c6052b53bbf40939d54123, + // r and s are the result of mod n, so they should be less than n and have length<=32 + byte[] rs = rOrS.toByteArray(); + if (rs.length == RS_LEN) { + return rs; + } else if (rs.length == RS_LEN + 1 && rs[0] == 0) { + return Arrays.copyOfRange(rs, 1, RS_LEN + 1); + } else if (rs.length < RS_LEN) { + byte[] result = new byte[RS_LEN]; + Arrays.fill(result, (byte) 0); + System.arraycopy(rs, 0, result, RS_LEN - rs.length, rs.length); + return result; + } else { + throw new CryptoException("Error rs: {}", Hex.toHexString(rs)); + } + } + // -------------------------------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/AbstractAsymmetricCrypto.java b/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/AbstractAsymmetricCrypto.java new file mode 100644 index 000000000..df850ec5c --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/AbstractAsymmetricCrypto.java @@ -0,0 +1,325 @@ +package cn.hutool.crypto.asymmetric; + +import java.io.InputStream; +import java.nio.charset.Charset; +import java.security.PrivateKey; +import java.security.PublicKey; + +import cn.hutool.core.codec.BCD; +import cn.hutool.core.codec.Base64; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.HexUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.CryptoException; +import cn.hutool.crypto.SecureUtil; + +public abstract class AbstractAsymmetricCrypto> extends BaseAsymmetric { + // ------------------------------------------------------------------ Constructor start + /** + * 构造 + * + * 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做加密或者解密 + * + * @param algorithm 算法 + * @param privateKey 私钥 + * @param publicKey 公钥 + * @since 3.1.1 + */ + public AbstractAsymmetricCrypto(String algorithm, PrivateKey privateKey, PublicKey publicKey) { + super(algorithm, privateKey, publicKey); + } + // ------------------------------------------------------------------ Constructor end + + // --------------------------------------------------------------------------------- Encrypt + /** + * 加密 + * + * @param data 被加密的bytes + * @param keyType 私钥或公钥 {@link KeyType} + * @return 加密后的bytes + */ + public abstract byte[] encrypt(byte[] data, KeyType keyType); + + /** + * 编码为Hex字符串 + * + * @param data 被加密的bytes + * @param keyType 私钥或公钥 {@link KeyType} + * @return Hex字符串 + */ + public String encryptHex(byte[] data, KeyType keyType) { + return HexUtil.encodeHexStr(encrypt(data, keyType)); + } + + /** + * 编码为Base64字符串 + * + * @param data 被加密的bytes + * @param keyType 私钥或公钥 {@link KeyType} + * @return Base64字符串 + * @since 4.0.1 + */ + public String encryptBase64(byte[] data, KeyType keyType) { + return Base64.encode(encrypt(data, keyType)); + } + + /** + * 加密 + * + * @param data 被加密的字符串 + * @param charset 编码 + * @param keyType 私钥或公钥 {@link KeyType} + * @return 加密后的bytes + */ + public byte[] encrypt(String data, String charset, KeyType keyType) { + return encrypt(StrUtil.bytes(data, charset), keyType); + } + + /** + * 加密 + * + * @param data 被加密的字符串 + * @param charset 编码 + * @param keyType 私钥或公钥 {@link KeyType} + * @return 加密后的bytes + */ + public byte[] encrypt(String data, Charset charset, KeyType keyType) { + return encrypt(StrUtil.bytes(data, charset), keyType); + } + + /** + * 加密,使用UTF-8编码 + * + * @param data 被加密的字符串 + * @param keyType 私钥或公钥 {@link KeyType} + * @return 加密后的bytes + */ + public byte[] encrypt(String data, KeyType keyType) { + return encrypt(StrUtil.bytes(data, CharsetUtil.CHARSET_UTF_8), keyType); + } + + /** + * 编码为Hex字符串 + * + * @param data 被加密的字符串 + * @param keyType 私钥或公钥 {@link KeyType} + * @return Hex字符串 + * @since 4.0.1 + */ + public String encryptHex(String data, KeyType keyType) { + return HexUtil.encodeHexStr(encrypt(data, keyType)); + } + + /** + * 编码为Hex字符串 + * + * @param data 被加密的bytes + * @param charset 编码 + * @param keyType 私钥或公钥 {@link KeyType} + * @return Hex字符串 + * @since 4.0.1 + */ + public String encryptHex(String data, Charset charset, KeyType keyType) { + return HexUtil.encodeHexStr(encrypt(data, charset, keyType)); + } + + /** + * 编码为Base64字符串,使用UTF-8编码 + * + * @param data 被加密的字符串 + * @param keyType 私钥或公钥 {@link KeyType} + * @return Base64字符串 + * @since 4.0.1 + */ + public String encryptBase64(String data, KeyType keyType) { + return Base64.encode(encrypt(data, keyType)); + } + + /** + * 编码为Base64字符串 + * + * @param data 被加密的字符串 + * @param keyType 私钥或公钥 {@link KeyType} + * @return Base64字符串 + * @since 4.0.1 + */ + public String encryptBase64(String data, Charset charset, KeyType keyType) { + return Base64.encode(encrypt(data, charset, keyType)); + } + + /** + * 加密 + * + * @param data 被加密的数据流 + * @param keyType 私钥或公钥 {@link KeyType} + * @return 加密后的bytes + * @throws IORuntimeException IO异常 + */ + public byte[] encrypt(InputStream data, KeyType keyType) throws IORuntimeException { + return encrypt(IoUtil.readBytes(data), keyType); + } + + /** + * 编码为Hex字符串 + * + * @param data 被加密的数据流 + * @param keyType 私钥或公钥 {@link KeyType} + * @return Hex字符串 + * @since 4.0.1 + */ + public String encryptHex(InputStream data, KeyType keyType) { + return HexUtil.encodeHexStr(encrypt(data, keyType)); + } + + /** + * 编码为Base64字符串 + * + * @param data 被加密的数据流 + * @param keyType 私钥或公钥 {@link KeyType} + * @return Base64字符串 + * @since 4.0.1 + */ + public String encryptBase64(InputStream data, KeyType keyType) { + return Base64.encode(encrypt(data, keyType)); + } + + /** + * 分组加密 + * + * @param data 数据 + * @param keyType 密钥类型 + * @return 加密后的密文 + * @throws CryptoException 加密异常 + * @since 4.1.0 + */ + public String encryptBcd(String data, KeyType keyType) { + return encryptBcd(data, keyType, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 分组加密 + * + * @param data 数据 + * @param keyType 密钥类型 + * @param charset 加密前编码 + * @return 加密后的密文 + * @throws CryptoException 加密异常 + * @since 4.1.0 + */ + public String encryptBcd(String data, KeyType keyType, Charset charset) { + return BCD.bcdToStr(encrypt(data, charset, keyType)); + } + + // --------------------------------------------------------------------------------- Decrypt + /** + * 解密 + * + * @param bytes 被解密的bytes + * @param keyType 私钥或公钥 {@link KeyType} + * @return 解密后的bytes + */ + public abstract byte[] decrypt(byte[] bytes, KeyType keyType); + + /** + * 解密 + * + * @param data 被解密的bytes + * @param keyType 私钥或公钥 {@link KeyType} + * @return 解密后的bytes + * @throws IORuntimeException IO异常 + */ + public byte[] decrypt(InputStream data, KeyType keyType) throws IORuntimeException { + return decrypt(IoUtil.readBytes(data), keyType); + } + + /** + * 从Hex或Base64字符串解密,编码为UTF-8格式 + * + * @param data Hex(16进制)或Base64字符串 + * @param keyType 私钥或公钥 {@link KeyType} + * @return 解密后的bytes + * @since 4.5.2 + */ + public byte[] decrypt(String data, KeyType keyType) { + return decrypt(SecureUtil.decode(data), keyType); + } + + /** + * 解密为字符串,密文需为Hex(16进制)或Base64字符串 + * + * @param data 数据,Hex(16进制)或Base64字符串 + * @param keyType 密钥类型 + * @param charset 加密前编码 + * @return 解密后的密文 + * @since 4.5.2 + */ + public String decryptStr(String data, KeyType keyType, Charset charset) { + return StrUtil.str(decrypt(data, keyType), charset); + } + + /** + * 解密为字符串,密文需为Hex(16进制)或Base64字符串 + * + * @param data 数据,Hex(16进制)或Base64字符串 + * @param keyType 密钥类型 + * @return 解密后的密文 + * @since 4.5.2 + */ + public String decryptStr(String data, KeyType keyType) { + return decryptStr(data, keyType, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 解密BCD + * + * @param data 数据 + * @param keyType 密钥类型 + * @return 解密后的密文 + * @since 4.1.0 + */ + public byte[] decryptFromBcd(String data, KeyType keyType) { + return decryptFromBcd(data, keyType, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 分组解密 + * + * @param data 数据 + * @param keyType 密钥类型 + * @param charset 加密前编码 + * @return 解密后的密文 + * @since 4.1.0 + */ + public byte[] decryptFromBcd(String data, KeyType keyType, Charset charset) { + final byte[] dataBytes = BCD.ascToBcd(StrUtil.bytes(data, charset)); + return decrypt(dataBytes, keyType); + } + + /** + * 解密为字符串,密文需为BCD格式 + * + * @param data 数据,BCD格式 + * @param keyType 密钥类型 + * @param charset 加密前编码 + * @return 解密后的密文 + * @since 4.5.2 + */ + public String decryptStrFromBcd(String data, KeyType keyType, Charset charset) { + return StrUtil.str(decryptFromBcd(data, keyType, charset), charset); + } + + /** + * 解密为字符串,密文需为BCD格式,编码为UTF-8格式 + * + * @param data 数据,BCD格式 + * @param keyType 密钥类型 + * @return 解密后的密文 + * @since 4.5.2 + */ + public String decryptStrFromBcd(String data, KeyType keyType) { + return decryptStrFromBcd(data, keyType, CharsetUtil.CHARSET_UTF_8); + } +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/AsymmetricAlgorithm.java b/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/AsymmetricAlgorithm.java new file mode 100644 index 000000000..ecd398392 --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/AsymmetricAlgorithm.java @@ -0,0 +1,37 @@ +package cn.hutool.crypto.asymmetric; + +/** + * 非对称算法类型
+ * see: https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyPairGenerator + * + * @author Looly + * + */ +public enum AsymmetricAlgorithm { + /** RSA算法 */ + RSA("RSA"), + /** RSA算法,此算法用了默认补位方式为RSA/ECB/PKCS1Padding */ + RSA_ECB_PKCS1("RSA/ECB/PKCS1Padding"), + /** RSA算法,此算法用了RSA/None/NoPadding */ + RSA_None("RSA/None/NoPadding"), + /** EC算法 */ + EC("EC"); + + private String value; + + /** + * 构造 + * @param value 算法字符表示,区分大小写 + */ + private AsymmetricAlgorithm(String value) { + this.value = value; + } + + /** + * 获取算法字符串表示,区分大小写 + * @return 算法字符串表示 + */ + public String getValue() { + return this.value; + } +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/AsymmetricCrypto.java b/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/AsymmetricCrypto.java new file mode 100644 index 000000000..e1f72d401 --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/AsymmetricCrypto.java @@ -0,0 +1,307 @@ +package cn.hutool.crypto.asymmetric; + +import java.io.IOException; +import java.security.Key; +import java.security.PrivateKey; +import java.security.PublicKey; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.io.FastByteArrayOutputStream; +import cn.hutool.crypto.CryptoException; +import cn.hutool.crypto.SecureUtil; +import cn.hutool.crypto.symmetric.SymmetricAlgorithm; + +/** + * 非对称加密算法 + * + *
+ * 1、签名:使用私钥加密,公钥解密。
+ * 用于让所有公钥所有者验证私钥所有者的身份并且用来防止私钥所有者发布的内容被篡改,但是不用来保证内容不被他人获得。
+ * 
+ * 2、加密:用公钥加密,私钥解密。
+ * 用于向公钥所有者发布信息,这个信息可能被他人篡改,但是无法被他人获得。
+ * 
+ * + * @author Looly + * + */ +public class AsymmetricCrypto extends AbstractAsymmetricCrypto { + + /** Cipher负责完成加密或解密工作 */ + protected Cipher cipher; + + /** 加密的块大小 */ + protected int encryptBlockSize = -1; + /** 解密的块大小 */ + protected int decryptBlockSize = -1; + + // ------------------------------------------------------------------ Constructor start + /** + * 构造,创建新的私钥公钥对 + * + * @param algorithm {@link SymmetricAlgorithm} + */ + public AsymmetricCrypto(AsymmetricAlgorithm algorithm) { + this(algorithm, (byte[]) null, (byte[]) null); + } + + /** + * 构造,创建新的私钥公钥对 + * + * @param algorithm 算法 + */ + public AsymmetricCrypto(String algorithm) { + this(algorithm, (byte[]) null, (byte[]) null); + } + + /** + * 构造 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做加密或者解密 + * + * @param algorithm {@link SymmetricAlgorithm} + * @param privateKeyStr 私钥Hex或Base64表示 + * @param publicKeyStr 公钥Hex或Base64表示 + */ + public AsymmetricCrypto(AsymmetricAlgorithm algorithm, String privateKeyStr, String publicKeyStr) { + this(algorithm.getValue(), SecureUtil.decode(privateKeyStr), SecureUtil.decode(publicKeyStr)); + } + + /** + * 构造 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做加密或者解密 + * + * @param algorithm {@link SymmetricAlgorithm} + * @param privateKey 私钥 + * @param publicKey 公钥 + */ + public AsymmetricCrypto(AsymmetricAlgorithm algorithm, byte[] privateKey, byte[] publicKey) { + this(algorithm.getValue(), privateKey, publicKey); + } + + /** + * 构造 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做加密或者解密 + * + * @param algorithm {@link SymmetricAlgorithm} + * @param privateKey 私钥 + * @param publicKey 公钥 + * @since 3.1.1 + */ + public AsymmetricCrypto(AsymmetricAlgorithm algorithm, PrivateKey privateKey, PublicKey publicKey) { + this(algorithm.getValue(), privateKey, publicKey); + } + + /** + * 构造 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做加密或者解密 + * + * @param algorithm 非对称加密算法 + * @param privateKeyBase64 私钥Base64 + * @param publicKeyBase64 公钥Base64 + */ + public AsymmetricCrypto(String algorithm, String privateKeyBase64, String publicKeyBase64) { + this(algorithm, Base64.decode(privateKeyBase64), Base64.decode(publicKeyBase64)); + } + + /** + * 构造 + * + * 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做加密或者解密 + * + * @param algorithm 算法 + * @param privateKey 私钥 + * @param publicKey 公钥 + */ + public AsymmetricCrypto(String algorithm, byte[] privateKey, byte[] publicKey) { + this(algorithm, // + SecureUtil.generatePrivateKey(algorithm, privateKey), // + SecureUtil.generatePublicKey(algorithm, publicKey)// + ); + } + + /** + * 构造 + * + * 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做加密或者解密 + * + * @param algorithm 算法 + * @param privateKey 私钥 + * @param publicKey 公钥 + * @since 3.1.1 + */ + public AsymmetricCrypto(String algorithm, PrivateKey privateKey, PublicKey publicKey) { + super(algorithm, privateKey, publicKey); + } + // ------------------------------------------------------------------ Constructor end + + /** + * 获取加密块大小 + * + * @return 加密块大小 + */ + public int getEncryptBlockSize() { + return encryptBlockSize; + } + + /** + * 设置加密块大小 + * + * @param encryptBlockSize 加密块大小 + */ + public void setEncryptBlockSize(int encryptBlockSize) { + this.encryptBlockSize = encryptBlockSize; + } + + /** + * 获取解密块大小 + * + * @return 解密块大小 + */ + public int getDecryptBlockSize() { + return decryptBlockSize; + } + + /** + * 设置解密块大小 + * + * @param decryptBlockSize 解密块大小 + */ + public void setDecryptBlockSize(int decryptBlockSize) { + this.decryptBlockSize = decryptBlockSize; + } + + @Override + public AsymmetricCrypto init(String algorithm, PrivateKey privateKey, PublicKey publicKey) { + super.init(algorithm, privateKey, publicKey); + initCipher(); + return this; + } + + // --------------------------------------------------------------------------------- Encrypt + /** + * 加密 + * + * @param data 被加密的bytes + * @param keyType 私钥或公钥 {@link KeyType} + * @return 加密后的bytes + */ + @Override + public byte[] encrypt(byte[] data, KeyType keyType) { + final Key key = getKeyByType(keyType); + final int maxBlockSize = this.encryptBlockSize < 0 ? data.length : this.encryptBlockSize; + + lock.lock(); + try { + cipher.init(Cipher.ENCRYPT_MODE, key); + return doFinal(data, maxBlockSize); + } catch (Exception e) { + throw new CryptoException(e); + } finally { + lock.unlock(); + } + } + + // --------------------------------------------------------------------------------- Decrypt + /** + * 解密 + * + * @param data 被解密的bytes + * @param keyType 私钥或公钥 {@link KeyType} + * @return 解密后的bytes + */ + @Override + public byte[] decrypt(byte[] data, KeyType keyType) { + final Key key = getKeyByType(keyType); + final int maxBlockSize = this.decryptBlockSize < 0 ? data.length : this.decryptBlockSize; + + lock.lock(); + try { + cipher.init(Cipher.DECRYPT_MODE, key); + return doFinal(data, maxBlockSize); + } catch (Exception e) { + throw new CryptoException(e); + } finally { + lock.unlock(); + } + } + + // --------------------------------------------------------------------------------- Getters and Setters + + /** + * 获得加密或解密器 + * + * @return 加密或解密 + */ + public Cipher getClipher() { + return cipher; + } + + /** + * 初始化{@link Cipher},默认尝试加载BC库 + * + * @since 4.5.2 + */ + protected void initCipher() { + this.cipher = SecureUtil.createCipher(algorithm); + } + + /** + * 加密或解密 + * + * @param data 被加密或解密的内容数据 + * @param maxBlockSize 最大块(分段)大小 + * @return 加密或解密后的数据 + * @throws IllegalBlockSizeException 分段异常 + * @throws BadPaddingException padding错误异常 + * @throws IOException IO异常,不会被触发 + */ + private byte[] doFinal(byte[] data, int maxBlockSize) throws IllegalBlockSizeException, BadPaddingException, IOException { + // 模长 + final int dataLength = data.length; + + // 不足分段 + if (dataLength <= maxBlockSize) { + return this.cipher.doFinal(data, 0, dataLength); + } + + // 分段解密 + return doFinalWithBlock(data, maxBlockSize); + } + + /** + * 分段加密或解密 + * + * @param data 数据 + * @param maxBlockSize 最大分段的段大小,不能为小于1 + * @return 加密或解密后的数据 + * @throws IllegalBlockSizeException 分段异常 + * @throws BadPaddingException padding错误异常 + * @throws IOException IO异常,不会被触发 + */ + private byte[] doFinalWithBlock(byte[] data, int maxBlockSize) throws IllegalBlockSizeException, BadPaddingException, IOException { + final int dataLength = data.length; + @SuppressWarnings("resource") + final FastByteArrayOutputStream out = new FastByteArrayOutputStream(); + + int offSet = 0; + // 剩余长度 + int remainLength = dataLength; + int blockSize; + // 对数据分段处理 + while (remainLength > 0) { + blockSize = Math.min(remainLength, maxBlockSize); + out.write(cipher.doFinal(data, offSet, blockSize)); + + offSet += blockSize; + remainLength = dataLength - offSet; + } + + return out.toByteArray(); + } +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/BaseAsymmetric.java b/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/BaseAsymmetric.java new file mode 100644 index 000000000..487c9470b --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/BaseAsymmetric.java @@ -0,0 +1,171 @@ +package cn.hutool.crypto.asymmetric; + +import java.security.Key; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import cn.hutool.core.codec.Base64; +import cn.hutool.crypto.CryptoException; +import cn.hutool.crypto.SecureUtil; + +/** + * 非对称基础,提供锁、私钥和公钥的持有 + * + * @author Looly + * @since 3.3.0 + */ +public class BaseAsymmetric>{ + + /** 算法 */ + protected String algorithm; + /** 公钥 */ + protected PublicKey publicKey; + /** 私钥 */ + protected PrivateKey privateKey; + /** 锁 */ + protected Lock lock = new ReentrantLock(); + + // ------------------------------------------------------------------ Constructor start + /** + * 构造 + * + * 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做加密或者解密 + * + * @param algorithm 算法 + * @param privateKey 私钥 + * @param publicKey 公钥 + * @since 3.1.1 + */ + public BaseAsymmetric(String algorithm, PrivateKey privateKey, PublicKey publicKey) { + init(algorithm, privateKey, publicKey); + } + // ------------------------------------------------------------------ Constructor end + + /** + * 初始化
+ * 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做加密(签名)或者解密(校验) + * + * @param algorithm 算法 + * @param privateKey 私钥 + * @param publicKey 公钥 + * @return this + */ + @SuppressWarnings("unchecked") + protected T init(String algorithm, PrivateKey privateKey, PublicKey publicKey) { + this.algorithm = algorithm; + + if (null == privateKey && null == publicKey) { + initKeys(); + } else { + if (null != privateKey) { + this.privateKey = privateKey; + } + if (null != publicKey) { + this.publicKey = publicKey; + } + } + return (T) this; + } + + /** + * 生成公钥和私钥 + * + * @return this + */ + @SuppressWarnings("unchecked") + public T initKeys() { + KeyPair keyPair = SecureUtil.generateKeyPair(this.algorithm); + this.publicKey = keyPair.getPublic(); + this.privateKey = keyPair.getPrivate(); + return (T) this; + } + + // --------------------------------------------------------------------------------- Getters and Setters + /** + * 获得公钥 + * + * @return 获得公钥 + */ + public PublicKey getPublicKey() { + return this.publicKey; + } + + /** + * 获得公钥 + * + * @return 获得公钥 + */ + public String getPublicKeyBase64() { + final PublicKey publicKey = getPublicKey(); + return (null == publicKey) ? null : Base64.encode(publicKey.getEncoded()); + } + + /** + * 设置公钥 + * + * @param publicKey 公钥 + * @return this + */ + @SuppressWarnings("unchecked") + public T setPublicKey(PublicKey publicKey) { + this.publicKey = publicKey; + return (T) this; + } + + /** + * 获得私钥 + * + * @return 获得私钥 + */ + public PrivateKey getPrivateKey() { + return this.privateKey; + } + + /** + * 获得私钥 + * + * @return 获得私钥 + */ + public String getPrivateKeyBase64() { + return Base64.encode(getPrivateKey().getEncoded()); + } + + /** + * 设置私钥 + * + * @param privateKey 私钥 + * @return this + */ + @SuppressWarnings("unchecked") + public T setPrivateKey(PrivateKey privateKey) { + this.privateKey = privateKey; + return (T) this; + } + + /** + * 根据密钥类型获得相应密钥 + * + * @param type 类型 {@link KeyType} + * @return {@link Key} + */ + protected Key getKeyByType(KeyType type) { + switch (type) { + case PrivateKey: + if (null == this.privateKey) { + throw new NullPointerException("Private key must not null when use it !"); + } + return this.privateKey; + case PublicKey: + if (null == this.publicKey) { + throw new NullPointerException("Public key must not null when use it !"); + } + return this.publicKey; + } + throw new CryptoException("Uknown key type: " + type); + } +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/KeyType.java b/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/KeyType.java new file mode 100644 index 000000000..55abdc9be --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/KeyType.java @@ -0,0 +1,11 @@ +package cn.hutool.crypto.asymmetric; + +/** + * 密钥类型 + * + * @author Looly + * + */ +public enum KeyType { + PrivateKey, PublicKey; +} \ No newline at end of file diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/RSA.java b/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/RSA.java new file mode 100644 index 000000000..321c61c49 --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/RSA.java @@ -0,0 +1,219 @@ +package cn.hutool.crypto.asymmetric; + +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.interfaces.RSAKey; +import java.security.spec.RSAPrivateKeySpec; +import java.security.spec.RSAPublicKeySpec; + +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.crypto.CryptoException; +import cn.hutool.crypto.SecureUtil; + +/** + *

+ * RSA公钥/私钥/签名加密解密 + *

+ *

+ * 罗纳德·李维斯特(Ron [R]ivest)、阿迪·萨莫尔(Adi [S]hamir)和伦纳德·阿德曼(Leonard [A]dleman) + *

+ *

+ * 由于非对称加密速度极其缓慢,一般文件不使用它来加密而是使用对称加密,
+ * 非对称加密算法可以用来对对称加密的密钥加密,这样保证密钥的安全也就保证了数据的安全 + *

+ * + * @author Looly + * + */ +public class RSA extends AsymmetricCrypto { + + /** 默认的RSA算法 */ + private static final AsymmetricAlgorithm ALGORITHM_RSA = AsymmetricAlgorithm.RSA_ECB_PKCS1; + + // ------------------------------------------------------------------ Static method start + /** + * 生成RSA私钥 + * + * @param modulus N特征值 + * @param privateExponent d特征值 + * @return {@link PrivateKey} + */ + public static PrivateKey generatePrivateKey(BigInteger modulus, BigInteger privateExponent) { + return SecureUtil.generatePrivateKey(ALGORITHM_RSA.getValue(), new RSAPrivateKeySpec(modulus, privateExponent)); + } + + /** + * 生成RSA公钥 + * + * @param modulus N特征值 + * @param publicExponent e特征值 + * @return {@link PublicKey} + */ + public static PublicKey generatePublicKey(BigInteger modulus, BigInteger publicExponent) { + return SecureUtil.generatePublicKey(ALGORITHM_RSA.getValue(), new RSAPublicKeySpec(modulus, publicExponent)); + } + // ------------------------------------------------------------------ Static method end + + // ------------------------------------------------------------------ Constructor start + /** + * 构造,生成新的私钥公钥对 + */ + public RSA() { + super(ALGORITHM_RSA); + } + + /** + * 构造,生成新的私钥公钥对 + * + * @param rsaAlgorithm 自定义RSA算法,例如RSA/ECB/PKCS1Padding + */ + public RSA(String rsaAlgorithm) { + super(rsaAlgorithm); + } + + /** + * 构造
+ * 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做加密或者解密 + * + * @param privateKeyStr 私钥Hex或Base64表示 + * @param publicKeyStr 公钥Hex或Base64表示 + */ + public RSA(String privateKeyStr, String publicKeyStr) { + super(ALGORITHM_RSA, privateKeyStr, publicKeyStr); + } + + /** + * 构造
+ * 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做加密或者解密 + * + * @param rsaAlgorithm 自定义RSA算法,例如RSA/ECB/PKCS1Padding + * @param privateKeyStr 私钥Hex或Base64表示 + * @param publicKeyStr 公钥Hex或Base64表示 + * @since 4.5.8 + */ + public RSA(String rsaAlgorithm, String privateKeyStr, String publicKeyStr) { + super(rsaAlgorithm, privateKeyStr, publicKeyStr); + } + + /** + * 构造
+ * 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做加密或者解密 + * + * @param privateKey 私钥 + * @param publicKey 公钥 + */ + public RSA(byte[] privateKey, byte[] publicKey) { + super(ALGORITHM_RSA, privateKey, publicKey); + } + + /** + * 构造
+ * 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做加密或者解密 + * + * @param modulus N特征值 + * @param privateExponent d特征值 + * @param publicExponent e特征值 + * @since 3.1.1 + */ + public RSA(BigInteger modulus, BigInteger privateExponent, BigInteger publicExponent) { + this(generatePrivateKey(modulus, privateExponent), generatePublicKey(modulus, publicExponent)); + } + + /** + * 构造
+ * 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做加密或者解密 + * + * @param privateKey 私钥 + * @param publicKey 公钥 + * @since 3.1.1 + */ + public RSA(PrivateKey privateKey, PublicKey publicKey) { + super(ALGORITHM_RSA, privateKey, publicKey); + } + + /** + * 构造
+ * 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做加密或者解密 + * + * @param rsaAlgorithm 自定义RSA算法,例如RSA/ECB/PKCS1Padding + * @param privateKey 私钥 + * @param publicKey 公钥 + * @since 4.5.8 + */ + public RSA(String rsaAlgorithm, PrivateKey privateKey, PublicKey publicKey) { + super(rsaAlgorithm, privateKey, publicKey); + } + // ------------------------------------------------------------------ Constructor end + + /** + * 分组加密 + * + * @param data 数据 + * @param keyType 密钥类型 + * @return 加密后的密文 + * @throws CryptoException 加密异常 + * @deprecated 请使用 {@link #encryptBcd(String, KeyType)} + */ + @Deprecated + public String encryptStr(String data, KeyType keyType) { + return encryptBcd(data, keyType, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 分组加密 + * + * @param data 数据 + * @param keyType 密钥类型 + * @param charset 加密前编码 + * @return 加密后的密文 + * @throws CryptoException 加密异常 + * @since 3.1.1 + * @deprecated 请使用 {@link #encryptBcd(String, KeyType, Charset)} + */ + @Deprecated + public String encryptStr(String data, KeyType keyType, Charset charset) { + return encryptBcd(data, keyType, charset); + } + + @Override + public byte[] encrypt(byte[] data, KeyType keyType) { + if (this.encryptBlockSize < 0) { + // 加密数据长度 <= 模长-11 + this.encryptBlockSize = ((RSAKey) getKeyByType(keyType)).getModulus().bitLength() / 8 - 11; + } + return super.encrypt(data, keyType); + } + + @Override + public byte[] decrypt(byte[] bytes, KeyType keyType) { + if (this.decryptBlockSize < 0) { + // 加密数据长度 <= 模长-11 + this.decryptBlockSize = ((RSAKey) getKeyByType(keyType)).getModulus().bitLength() / 8; + } + return super.decrypt(bytes, keyType); + } + + @Override + protected void initCipher() { + try { + super.initCipher(); + } catch (CryptoException e) { + final Throwable cause = e.getCause(); + if(cause instanceof NoSuchAlgorithmException) { + // 在Linux下,未引入BC库可能会导致RSA/ECB/PKCS1Padding算法无法找到,此时使用默认算法 + this.algorithm = AsymmetricAlgorithm.RSA.getValue(); + super.initCipher(); + } + throw e; + } + } +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/SM2.java b/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/SM2.java new file mode 100644 index 000000000..7cf2efc9b --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/SM2.java @@ -0,0 +1,334 @@ +package cn.hutool.crypto.asymmetric; + +import java.security.InvalidKeyException; +import java.security.PrivateKey; +import java.security.PublicKey; + +import org.bouncycastle.crypto.CipherParameters; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.crypto.params.ParametersWithID; +import org.bouncycastle.crypto.params.ParametersWithRandom; +import org.bouncycastle.crypto.signers.SM2Signer; +import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil; + +import cn.hutool.crypto.CryptoException; +import cn.hutool.crypto.SecureUtil; +import cn.hutool.crypto.asymmetric.SM2Engine.SM2Mode; + +/** + * 国密SM2算法实现,基于BC库
+ * SM2算法只支持公钥加密,私钥解密
+ * 参考:https://blog.csdn.net/pridas/article/details/86118774 + * + * @author looly + * @since 4.3.2 + */ +public class SM2 extends AbstractAsymmetricCrypto { + + /** 算法EC */ + private static final String ALGORITHM_SM2 = "SM2"; + + protected SM2Engine engine; + protected SM2Signer signer; + + private SM2Mode mode; + private ECPublicKeyParameters publicKeyParams; + private ECPrivateKeyParameters privateKeyParams; + + // ------------------------------------------------------------------ Constructor start + /** + * 构造,生成新的私钥公钥对 + */ + public SM2() { + this((byte[]) null, (byte[]) null); + } + + /** + * 构造
+ * 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做加密或者解密 + * + * @param privateKeyStr 私钥Hex或Base64表示 + * @param publicKeyStr 公钥Hex或Base64表示 + */ + public SM2(String privateKeyStr, String publicKeyStr) { + this(SecureUtil.decode(privateKeyStr), SecureUtil.decode(publicKeyStr)); + } + + /** + * 构造
+ * 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做加密或者解密 + * + * @param privateKey 私钥 + * @param publicKey 公钥 + */ + public SM2(byte[] privateKey, byte[] publicKey) { + this(// + SecureUtil.generatePrivateKey(ALGORITHM_SM2, privateKey), // + SecureUtil.generatePublicKey(ALGORITHM_SM2, publicKey)// + ); + } + + /** + * 构造
+ * 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做加密或者解密 + * + * @param privateKey 私钥 + * @param publicKey 公钥 + */ + public SM2(PrivateKey privateKey, PublicKey publicKey) { + super(ALGORITHM_SM2, privateKey, publicKey); + } + // ------------------------------------------------------------------ Constructor end + + /** + * 初始化
+ * 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做加密(签名)或者解密(校验) + * + * @param privateKey 私钥 + * @param publicKey 公钥 + * @return this + */ + public SM2 init(PrivateKey privateKey, PublicKey publicKey) { + return this.init(ALGORITHM_SM2, privateKey, publicKey); + } + + @Override + protected SM2 init(String algorithm, PrivateKey privateKey, PublicKey publicKey) { + super.init(algorithm, privateKey, publicKey); + return initCipherParams(); + } + + // --------------------------------------------------------------------------------- Encrypt + /** + * 加密,SM2非对称加密的结果由C1,C2,C3三部分组成,其中: + * + *
+	 * C1 生成随机数的计算出的椭圆曲线点
+	 * C2 密文数据
+	 * C3 SM3的摘要值
+	 * 
+ * + * @param data 被加密的bytes + * @param keyType 私钥或公钥 {@link KeyType} + * @return 加密后的bytes + * @throws CryptoException 包括InvalidKeyException和InvalidCipherTextException的包装异常 + */ + @Override + public byte[] encrypt(byte[] data, KeyType keyType) throws CryptoException { + if (KeyType.PublicKey != keyType) { + throw new IllegalArgumentException("Encrypt is only support by public key"); + } + ckeckKey(keyType); + + lock.lock(); + final SM2Engine engine = getEngine(); + try { + engine.init(true, new ParametersWithRandom(getCipherParameters(keyType))); + return engine.processBlock(data, 0, data.length); + } finally { + lock.unlock(); + } + } + + // --------------------------------------------------------------------------------- Decrypt + /** + * 解密 + * + * @param data SM2密文,实际包含三部分:ECC公钥、真正的密文、公钥和原文的SM3-HASH值 + * @param keyType 私钥或公钥 {@link KeyType} + * @return 加密后的bytes + * @throws CryptoException 包括InvalidKeyException和InvalidCipherTextException的包装异常 + */ + @Override + public byte[] decrypt(byte[] data, KeyType keyType) throws CryptoException { + if (KeyType.PrivateKey != keyType) { + throw new IllegalArgumentException("Decrypt is only support by private key"); + } + ckeckKey(keyType); + + lock.lock(); + final SM2Engine engine = getEngine(); + try { + engine.init(false, getCipherParameters(keyType)); + return engine.processBlock(data, 0, data.length); + } finally { + lock.unlock(); + } + } + + // --------------------------------------------------------------------------------- Sign and Verify + /** + * 用私钥对信息生成数字签名 + * + * @param data 加密数据 + * @return 签名 + */ + public byte[] sign(byte[] data) { + return sign(data, null); + } + + /** + * 用私钥对信息生成数字签名 + * + * @param data 加密数据 + * @param id 可以为null,若为null,则默认withId为字节数组:"1234567812345678".getBytes() + * @return 签名 + */ + public byte[] sign(byte[] data, byte[] id) { + lock.lock(); + final SM2Signer signer = getSigner(); + try { + CipherParameters param = new ParametersWithRandom(getCipherParameters(KeyType.PrivateKey)); + if (id != null) { + param = new ParametersWithID(param, id); + } + signer.init(true, param); + signer.update(data, 0, data.length); + return signer.generateSignature(); + } catch (Exception e) { + throw new CryptoException(e); + } finally { + lock.unlock(); + } + } + + /** + * 用公钥检验数字签名的合法性 + * + * @param data 数据 + * @param sign 签名 + * @return 是否验证通过 + */ + public boolean verify(byte[] data, byte[] sign) { + return verify(data, sign, null); + } + + /** + * 用公钥检验数字签名的合法性 + * + * @param data 数据 + * @param sign 签名 + * @param id 可以为null,若为null,则默认withId为字节数组:"1234567812345678".getBytes() + * @return 是否验证通过 + */ + public boolean verify(byte[] data, byte[] sign, byte[] id) { + lock.lock(); + final SM2Signer signer = getSigner(); + try { + CipherParameters param = getCipherParameters(KeyType.PublicKey); + if (id != null) { + param = new ParametersWithID(param, id); + } + signer.init(false, param); + signer.update(data, 0, data.length); + return signer.verifySignature(sign); + } catch (Exception e) { + throw new CryptoException(e); + } finally { + lock.unlock(); + } + } + + /** + * 设置加密类型 + * + * @param mode {@link SM2Mode} + * @return this + */ + public SM2 setMode(SM2Mode mode) { + this.mode = mode; + if (null != this.engine) { + this.engine.setMode(mode); + } + return this; + } + + // ------------------------------------------------------------------------------------------------------------------------- Private method start + /** + * 初始化加密解密参数 + * + * @return this + */ + private SM2 initCipherParams() { + try { + if (null != this.publicKey) { + this.publicKeyParams = (ECPublicKeyParameters) ECUtil.generatePublicKeyParameter(this.publicKey); + } + if (null != privateKey) { + this.privateKeyParams = (ECPrivateKeyParameters) ECUtil.generatePrivateKeyParameter(this.privateKey); + } + } catch (InvalidKeyException e) { + throw new CryptoException(e); + } + + return this; + } + + /** + * 获取密钥类型对应的加密参数对象{@link CipherParameters} + * + * @param keyType Key类型枚举,包括私钥或公钥 + * @return {@link CipherParameters} + */ + private CipherParameters getCipherParameters(KeyType keyType) { + switch (keyType) { + case PublicKey: + return this.publicKeyParams; + case PrivateKey: + return this.privateKeyParams; + } + + return null; + } + + /** + * 检查对应类型的Key是否存在 + * + * @param keyType key类型 + */ + private void ckeckKey(KeyType keyType) { + switch (keyType) { + case PublicKey: + if (null == this.publicKey) { + throw new NullPointerException("No public key provided"); + } + break; + case PrivateKey: + if (null == this.privateKey) { + throw new NullPointerException("No private key provided"); + } + break; + } + } + + /** + * 获取{@link SM2Engine} + * + * @return {@link SM2Engine} + */ + private SM2Engine getEngine() { + if (null == this.engine) { + this.engine = new SM2Engine(this.mode); + } + return this.engine; + } + + /** + * 获取{@link SM2Signer} + * + * @return {@link SM2Signer} + */ + private SM2Signer getSigner() { + if (null == this.signer) { + this.signer = new SM2Signer(); + } + return this.signer; + } + + // ------------------------------------------------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/SM2Engine.java b/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/SM2Engine.java new file mode 100644 index 000000000..d8795ee4e --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/SM2Engine.java @@ -0,0 +1,359 @@ +package cn.hutool.crypto.asymmetric; + +import java.math.BigInteger; +import java.util.Random; + +import org.bouncycastle.crypto.CipherParameters; +import org.bouncycastle.crypto.CryptoServicesRegistrar; +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.digests.SM3Digest; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.crypto.params.ECKeyParameters; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.crypto.params.ParametersWithRandom; +import org.bouncycastle.math.ec.ECConstants; +import org.bouncycastle.math.ec.ECFieldElement; +import org.bouncycastle.math.ec.ECMultiplier; +import org.bouncycastle.math.ec.ECPoint; +import org.bouncycastle.math.ec.FixedPointCombMultiplier; +import org.bouncycastle.util.Arrays; +import org.bouncycastle.util.BigIntegers; +import org.bouncycastle.util.Memoable; +import org.bouncycastle.util.Pack; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.crypto.CryptoException; + +/** + * SM2加密解密引擎,来自Bouncy Castle库的SM2Engine类改造
+ * SM2加密后的数据格式为(两种模式): + * + *
+ * curve(C1) | data(C2) | digest(C3)
+ * curve(C1) | digest(C3) | data(C2)
+ * 
+ * + * @author looly, bouncycastle + * @since 4.5.0 + */ +public class SM2Engine { + + private final Digest digest; + + private boolean forEncryption; + private ECKeyParameters ecKey; + private ECDomainParameters ecParams; + private int curveLength; + private Random random; + /** 加密解密模式 */ + private SM2Mode mode; + + /** + * 构造 + */ + public SM2Engine() { + this(new SM3Digest()); + } + + /** + * 构造 + * + * @param mode SM2密钥生成模式,可选C1C2C3和C1C3C2 + */ + public SM2Engine(SM2Mode mode) { + this(new SM3Digest(), mode); + } + + /** + * 构造 + * + * @param digest 摘要算法啊 + */ + public SM2Engine(Digest digest) { + this(digest, null); + } + + /** + * 构造 + * + * @param digest 摘要算法啊 + * @param mode SM2密钥生成模式,可选C1C2C3和C1C3C2 + */ + public SM2Engine(Digest digest, SM2Mode mode) { + this.digest = digest; + this.mode = ObjectUtil.defaultIfNull(mode, SM2Mode.C1C3C2); + } + + /** + * 初始化引擎 + * + * @param forEncryption 是否为加密模式 + * @param param {@link CipherParameters},此处应为{@link ParametersWithRandom}(加密时)或{@link ECKeyParameters}(解密时) + */ + public void init(boolean forEncryption, CipherParameters param) { + this.forEncryption = forEncryption; + + if (param instanceof ParametersWithRandom) { + final ParametersWithRandom rParam = (ParametersWithRandom) param; + this.ecKey = (ECKeyParameters) rParam.getParameters(); + this.random = rParam.getRandom(); + } else { + this.ecKey = (ECKeyParameters) param; + } + this.ecParams = this.ecKey.getParameters(); + + if (forEncryption) { + // 检查曲线点 + final ECPoint ecPoint = ((ECPublicKeyParameters) ecKey).getQ().multiply(ecParams.getH()); + if (ecPoint.isInfinity()) { + throw new IllegalArgumentException("invalid key: [h]Q at infinity"); + } + + // 检查随机参数 + if (null == this.random) { + this.random = CryptoServicesRegistrar.getSecureRandom(); + } + } + + // 曲线位长度 + this.curveLength = (this.ecParams.getCurve().getFieldSize() + 7) / 8; + } + + /** + * 处理块,包括加密和解密 + * + * @param in 数据 + * @param inOff 数据开始位置 + * @param inLen 数据长度 + * @return 结果 + */ + public byte[] processBlock(byte[] in, int inOff, int inLen) { + if (forEncryption) { + return encrypt(in, inOff, inLen); + } else { + return decrypt(in, inOff, inLen); + } + } + + /** + * 设置加密类型 + * + * @param mode {@link SM2Mode} + * @return this + */ + public SM2Engine setMode(SM2Mode mode) { + this.mode = mode; + return this; + } + + /** + * SM2算法模式
+ * 在SM2算法中,C1C2C3为旧标准模式,C1C3C2为新标准模式 + * + * @author looly + * + */ + public static enum SM2Mode { + C1C2C3, C1C3C2; + } + + protected ECMultiplier createBasePointMultiplier() { + return new FixedPointCombMultiplier(); + } + + // --------------------------------------------------------------------------------------------------- Private method start + /** + * 加密 + * + * @param in 数据 + * @param inOff 位置 + * @param inLen 长度 + * @return 密文 + */ + private byte[] encrypt(byte[] in, int inOff, int inLen) { + // 加密数据 + byte[] c2 = new byte[inLen]; + System.arraycopy(in, inOff, c2, 0, c2.length); + + final ECMultiplier multiplier = createBasePointMultiplier(); + + byte[] c1; + ECPoint kPB; + BigInteger k; + do { + k = nextK(); + // 产生随机数计算出曲线点C1 + c1 = multiplier.multiply(ecParams.getG(), k).normalize().getEncoded(false); + kPB = ((ECPublicKeyParameters) ecKey).getQ().multiply(k).normalize(); + kdf(kPB, c2); + } while (notEncrypted(c2, in, inOff)); + + // 杂凑值,效验数据 + byte[] c3 = new byte[digest.getDigestSize()]; + + addFieldElement(kPB.getAffineXCoord()); + this.digest.update(in, inOff, inLen); + addFieldElement(kPB.getAffineYCoord()); + + this.digest.doFinal(c3, 0); + + // 按照对应模式输出结果 + switch (mode) { + case C1C3C2: + return Arrays.concatenate(c1, c3, c2); + default: + return Arrays.concatenate(c1, c2, c3); + } + } + + /** + * 解密,只支持私钥解密 + * + * @param in 密文 + * @param inOff 位置 + * @param inLen 长度 + * @return 解密后的内容 + */ + private byte[] decrypt(byte[] in, int inOff, int inLen) { + // 获取曲线点 + final byte[] c1 = new byte[this.curveLength * 2 + 1]; + System.arraycopy(in, inOff, c1, 0, c1.length); + + ECPoint c1P = this.ecParams.getCurve().decodePoint(c1); + if (c1P.multiply(this.ecParams.getH()).isInfinity()) { + throw new CryptoException("[h]C1 at infinity"); + } + c1P = c1P.multiply(((ECPrivateKeyParameters) ecKey).getD()).normalize(); + + final int digestSize = this.digest.getDigestSize(); + + // 解密C2数据 + final byte[] c2 = new byte[inLen - c1.length - digestSize]; + + if (SM2Mode.C1C3C2 == this.mode) { + // C2位于第三部分 + System.arraycopy(in, inOff + c1.length + digestSize, c2, 0, c2.length); + } else { + // C2位于第二部分 + System.arraycopy(in, inOff + c1.length, c2, 0, c2.length); + } + kdf(c1P, c2); + + // 使用摘要验证C2数据 + final byte[] c3 = new byte[digestSize]; + + addFieldElement(c1P.getAffineXCoord()); + this.digest.update(c2, 0, c2.length); + addFieldElement(c1P.getAffineYCoord()); + this.digest.doFinal(c3, 0); + + int check = 0; + for (int i = 0; i != c3.length; i++) { + check |= c3[i] ^ in[inOff + c1.length + ((SM2Mode.C1C3C2 == this.mode) ? 0 : c2.length) + i]; + } + + Arrays.fill(c1, (byte) 0); + Arrays.fill(c3, (byte) 0); + + if (check != 0) { + Arrays.fill(c2, (byte) 0); + throw new CryptoException("invalid cipher text"); + } + + return c2; + } + + private boolean notEncrypted(byte[] encData, byte[] in, int inOff) { + for (int i = 0; i != encData.length; i++) { + if (encData[i] != in[inOff + i]) { + return false; + } + } + return true; + } + + /** + * 解密数据 + * + * @param c1 c1点 + * @param encData 密文 + */ + private void kdf(ECPoint c1, byte[] encData) { + final Digest digest = this.digest; + int digestSize = digest.getDigestSize(); + byte[] buf = new byte[Math.max(4, digestSize)]; + int off = 0; + + Memoable memo = null; + Memoable copy = null; + + if (digest instanceof Memoable) { + addFieldElement(c1.getAffineXCoord()); + addFieldElement(c1.getAffineYCoord()); + memo = (Memoable) digest; + copy = memo.copy(); + } + + int ct = 0; + + while (off < encData.length) { + if (memo != null) { + memo.reset(copy); + } else { + addFieldElement(c1.getAffineXCoord()); + addFieldElement(c1.getAffineYCoord()); + } + + Pack.intToBigEndian(++ct, buf, 0); + digest.update(buf, 0, 4); + digest.doFinal(buf, 0); + + int xorLen = Math.min(digestSize, encData.length - off); + xor(encData, buf, off, xorLen); + off += xorLen; + } + } + + /** + * 异或 + * + * @param data 数据 + * @param kdfOut kdf输出值 + * @param dOff d偏移 + * @param dRemaining d剩余 + */ + private void xor(byte[] data, byte[] kdfOut, int dOff, int dRemaining) { + for (int i = 0; i != dRemaining; i++) { + data[dOff + i] ^= kdfOut[i]; + } + } + + /** + * 下一个K值 + * + * @return K值 + */ + private BigInteger nextK() { + final int qBitLength = this.ecParams.getN().bitLength(); + + BigInteger k; + do { + k = new BigInteger(qBitLength, this.random); + } while (k.equals(ECConstants.ZERO) || k.compareTo(this.ecParams.getN()) >= 0); + + return k; + } + + /** + * 增加字段节点 + * + * @param digest + * @param v + */ + private void addFieldElement(ECFieldElement v) { + final byte[] p = BigIntegers.asUnsignedByteArray(this.curveLength, v.toBigInteger()); + this.digest.update(p, 0, p.length); + } + // --------------------------------------------------------------------------------------------------- Private method start +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/Sign.java b/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/Sign.java new file mode 100644 index 000000000..281e10bf0 --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/Sign.java @@ -0,0 +1,257 @@ +package cn.hutool.crypto.asymmetric; + +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Set; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.crypto.CryptoException; +import cn.hutool.crypto.SecureUtil; + +/** + * 签名包装,{@link Signature} 包装类 + * + * @author looly + * @since 3.3.0 + */ +public class Sign extends BaseAsymmetric { + + /** 签名,用于签名和验证 */ + protected Signature signature; + + // ------------------------------------------------------------------ Constructor start + /** + * 构造,创建新的私钥公钥对 + * + * @param algorithm {@link SignAlgorithm} + */ + public Sign(SignAlgorithm algorithm) { + this(algorithm, (byte[]) null, (byte[]) null); + } + + /** + * 构造,创建新的私钥公钥对 + * + * @param algorithm 算法 + */ + public Sign(String algorithm) { + this(algorithm, (byte[]) null, (byte[]) null); + } + + /** + * 构造 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做签名或验证 + * + * @param algorithm {@link SignAlgorithm} + * @param privateKeyStr 私钥Hex或Base64表示 + * @param publicKeyStr 公钥Hex或Base64表示 + */ + public Sign(SignAlgorithm algorithm, String privateKeyStr, String publicKeyStr) { + this(algorithm.getValue(), SecureUtil.decode(privateKeyStr), SecureUtil.decode(publicKeyStr)); + } + + /** + * 构造 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做签名或验证 + * + * @param algorithm {@link SignAlgorithm} + * @param privateKey 私钥 + * @param publicKey 公钥 + */ + public Sign(SignAlgorithm algorithm, byte[] privateKey, byte[] publicKey) { + this(algorithm.getValue(), privateKey, publicKey); + } + + /** + * 构造 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做签名或验证 + * + * @param algorithm {@link SignAlgorithm} + * @param keyPair 密钥对(包括公钥和私钥) + */ + public Sign(SignAlgorithm algorithm, KeyPair keyPair) { + this(algorithm.getValue(), keyPair); + } + + /** + * 构造 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做签名或验证 + * + * @param algorithm {@link SignAlgorithm} + * @param privateKey 私钥 + * @param publicKey 公钥 + */ + public Sign(SignAlgorithm algorithm, PrivateKey privateKey, PublicKey publicKey) { + this(algorithm.getValue(), privateKey, publicKey); + } + + /** + * 构造 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做签名或验证 + * + * @param algorithm 非对称加密算法 + * @param privateKeyBase64 私钥Base64 + * @param publicKeyBase64 公钥Base64 + */ + public Sign(String algorithm, String privateKeyBase64, String publicKeyBase64) { + this(algorithm, Base64.decode(privateKeyBase64), Base64.decode(publicKeyBase64)); + } + + /** + * 构造 + * + * 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做签名或验证 + * + * @param algorithm 算法 + * @param privateKey 私钥 + * @param publicKey 公钥 + */ + public Sign(String algorithm, byte[] privateKey, byte[] publicKey) { + this(algorithm, // + SecureUtil.generatePrivateKey(algorithm, privateKey), // + SecureUtil.generatePublicKey(algorithm, publicKey)// + ); + } + + /** + * 构造 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做签名或验证 + * + * @param algorithm 算法,见{@link SignAlgorithm} + * @param keyPair 密钥对(包括公钥和私钥) + */ + public Sign(String algorithm, KeyPair keyPair) { + this(algorithm, keyPair.getPrivate(), keyPair.getPublic()); + } + + /** + * 构造 + * + * 私钥和公钥同时为空时生成一对新的私钥和公钥
+ * 私钥和公钥可以单独传入一个,如此则只能使用此钥匙来做签名或验证 + * + * @param algorithm 算法 + * @param privateKey 私钥 + * @param publicKey 公钥 + */ + public Sign(String algorithm, PrivateKey privateKey, PublicKey publicKey) { + super(algorithm, privateKey, publicKey); + } + // ------------------------------------------------------------------ Constructor end + + /** + * 初始化 + * + * @param algorithm 算法 + * @param privateKey 私钥 + * @param publicKey 公钥 + * @return this + */ + @Override + public Sign init(String algorithm, PrivateKey privateKey, PublicKey publicKey) { + try { + signature = Signature.getInstance(algorithm); + } catch (NoSuchAlgorithmException e) { + throw new CryptoException(e); + } + super.init(algorithm, privateKey, publicKey); + return this; + } + + // --------------------------------------------------------------------------------- Sign and Verify + /** + * 用私钥对信息生成数字签名 + * + * @param data 加密数据 + * @return 签名 + */ + public byte[] sign(byte[] data) { + lock.lock(); + try { + signature.initSign(this.privateKey); + signature.update(data); + return signature.sign(); + } catch (Exception e) { + throw new CryptoException(e); + } finally { + lock.unlock(); + } + } + + /** + * 用公钥检验数字签名的合法性 + * + * @param data 数据 + * @param sign 签名 + * @return 是否验证通过 + */ + public boolean verify(byte[] data, byte[] sign) { + lock.lock(); + try { + signature.initVerify(this.publicKey); + signature.update(data); + return signature.verify(sign); + } catch (Exception e) { + throw new CryptoException(e); + } finally { + lock.unlock(); + } + } + + /** + * 获得签名对象 + * + * @return {@link Signature} + */ + public Signature getSignature() { + return signature; + } + + /** + * 设置签名 + * + * @param signature 签名对象 {@link Signature} + * @return 自身 {@link AsymmetricCrypto} + */ + public Sign setSignature(Signature signature) { + this.signature = signature; + return this; + } + + /** + * 设置{@link Certificate} 为PublicKey
+ * 如果Certificate是X509Certificate,我们需要检查是否有密钥扩展 + * + * @param certificate {@link Certificate} + * @return this + */ + public Sign setCertificate(Certificate certificate) { + // If the certificate is of type X509Certificate, + // we should check whether it has a Key Usage + // extension marked as critical. + if (certificate instanceof X509Certificate) { + // Check whether the cert has a key usage extension + // marked as a critical extension. + // The OID for KeyUsage extension is 2.5.29.15. + final X509Certificate cert = (X509Certificate) certificate; + final Set critSet = cert.getCriticalExtensionOIDs(); + + if (CollUtil.isNotEmpty(critSet) && critSet.contains("2.5.29.15")) { + final boolean[] keyUsageInfo = cert.getKeyUsage(); + // keyUsageInfo[0] is for digitalSignature. + if ((keyUsageInfo != null) && (keyUsageInfo[0] == false)) { + throw new CryptoException("Wrong key usage"); + } + } + } + this.publicKey = certificate.getPublicKey(); + return this; + } +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/SignAlgorithm.java b/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/SignAlgorithm.java new file mode 100644 index 000000000..8499ec2c1 --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/SignAlgorithm.java @@ -0,0 +1,55 @@ +package cn.hutool.crypto.asymmetric; + +/** + * 签名算法类型
+ * see: https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Signature + * + * @author Looly + * + */ +public enum SignAlgorithm { + // The RSA signature algorithm + NONEwithRSA("NONEwithRSA"), // + + // The MD2/MD5 with RSA Encryption signature algorithm + MD2withRSA("MD2withRSA"), // + MD5withRSA("MD5withRSA"), // + + // The signature algorithm with SHA-* and the RSA + SHA1withRSA("SHA1withRSA"), // + SHA256withRSA("SHA256withRSA"), // + SHA384withRSA("SHA384withRSA"), // + SHA512withRSA("SHA512withRSA"), // + + // The Digital Signature Algorithm + NONEwithDSA("NONEwithDSA"), // + // The DSA with SHA-1 signature algorithm + SHA1withDSA("SHA1withDSA"), // + + // The ECDSA signature algorithms + NONEwithECDSA("NONEwithECDSA"), // + SHA1withECDSA("SHA1withECDSA"), // + SHA256withECDSA("SHA256withECDSA"), // + SHA384withECDSA("SHA384withECDSA"), // + SHA512withECDSA("SHA512withECDSA");// + + private String value; + + /** + * 构造 + * + * @param value 算法字符表示,区分大小写 + */ + private SignAlgorithm(String value) { + this.value = value; + } + + /** + * 获取算法字符串表示,区分大小写 + * + * @return 算法字符串表示 + */ + public String getValue() { + return this.value; + } +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/package-info.java b/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/package-info.java new file mode 100644 index 000000000..cf3e90511 --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/package-info.java @@ -0,0 +1,7 @@ +/** + * 非对称加密的实现,包括RSA等 + * + * @author looly + * + */ +package cn.hutool.crypto.asymmetric; \ No newline at end of file diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/digest/BCrypt.java b/hutool-crypto/src/main/java/cn/hutool/crypto/digest/BCrypt.java new file mode 100644 index 000000000..c5afde246 --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/digest/BCrypt.java @@ -0,0 +1,521 @@ +package cn.hutool.crypto.digest; + +import java.security.SecureRandom; + +import cn.hutool.core.util.CharsetUtil; + +/** + * BCrypt加密算法实现。由它加密的文件可在所有支持的操作系统和处理器上进行转移。它的口令必须是8至56个字符,并将在内部被转化为448位的密钥。 + *

+ * 此类来自于https://github.com/jeremyh/jBCrypt/ + *

+ * 使用方法如下: + *

+ * +* String pw_hash = BCrypt.hashpw(plain_password, BCrypt.gensalt());
+*
+ *

+ * 使用checkpw方法检查被加密的字符串是否与原始字符串匹配: + *

+ * +* BCrypt.checkpw(candidate_password, stored_hash); +* + *

+ * gensalt方法提供了可选参数 (log_rounds) 来定义加盐多少,也决定了加密的复杂度: + *

+ * +* String strong_salt = BCrypt.gensalt(10)
+* String stronger_salt = BCrypt.gensalt(12)
+*
+ * + * @author Damien Miller + * @since 4.1.1 + */ +public class BCrypt { + // BCrypt parameters + private static final int GENSALT_DEFAULT_LOG2_ROUNDS = 10; + private static final int BCRYPT_SALT_LEN = 16; + + // Blowfish parameters + private static final int BLOWFISH_NUM_ROUNDS = 16; + + // Initial contents of key schedule + private static final int P_orig[] = { 0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, 0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89, 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, 0xc0ac29b7, + 0xc97c50dd, 0x3f84d5b5, 0xb5470917, 0x9216d5d9, 0x8979fb1b }; + private static final int S_orig[] = { 0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, 0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99, 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, 0x636920d8, + 0x71574e69, 0xa458fea3, 0xf4933d7e, 0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee, 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, + 0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e, 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, 0x55ca396a, 0x2aab10b6, 0xb4cc5c34, + 0x1141e8ce, 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a, 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, 0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677, 0x3b8f4898, 0x6b4bb9af, + 0xc4bfe81b, 0x66282193, 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, 0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88, 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, 0x2e0b4482, + 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, 0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0, 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, + 0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88, 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe, 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, 0x4ed3aa62, 0x363f7706, 0x1bfedf72, + 0x429b023d, 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, 0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba, 0xc1a94fb6, 0x409f60c4, + 0x5e5c9ec2, 0x196a2463, 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, 0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09, 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, 0xc0cba857, + 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, 0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279, 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, + 0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82, 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, 0x695b27b0, 0xbbca58c8, 0xe1ffa35d, + 0xb8f011a0, 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, 0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8, 0xef20cada, 0x36774c01, + 0xd07e9efe, 0x2bf11fb4, 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, 0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7, 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c, 0x4fad5ea0, + 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, 0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1, 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, + 0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477, 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, 0x00250e2d, 0x2071b35e, 0x226800bb, + 0x57b8e0af, 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, 0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41, 0xb3472dca, 0x7b14a94a, + 0x1b510052, 0x9a532915, 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, 0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915, 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, 0x53b02d5d, + 0xa99f8fa1, 0x08ba4799, 0x6e85076a, 0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, 0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266, 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, + 0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e, 0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6, 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, 0x4cdd2086, 0x8470eb26, 0x6382e9c6, + 0x021ecc5e, 0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1, 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, 0xb03ada37, 0xf0500c0d, + 0xf01c1f04, 0x0200b3ff, 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, 0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, 0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7, 0xa9446146, + 0x0fd0030e, 0xecc8c73e, 0xa4751e41, 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, 0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf, 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af, + 0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, 0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87, 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, 0xec7aec3a, 0xdb851dfa, 0x63094366, + 0xc464c3d2, 0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16, 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd, 0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, 0x043556f1, 0xd7a3c76b, + 0x3c11183b, 0x5924a509, 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, 0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f, 0x803e89d6, + 0x5266c825, 0x2e4cc978, 0x9c10b36a, 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, 0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960, 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, + 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, 0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802, 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, 0x1521b628, 0x29076170, 0xecdd4775, + 0x619f1510, 0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf, 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e, 0x648b1eaf, 0x19bdf0ca, + 0xa02369b9, 0x655abb50, 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, 0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281, 0x0e358829, + 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, 0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128, 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73, + 0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, 0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0, 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, 0xd81e799e, 0x86854dc7, 0xe44b476a, + 0x3d816250, 0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3, 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, 0x58428d2a, 0x0c55f5ea, + 0x1dadf43e, 0x233f7061, 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, 0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735, 0xa969a7aa, + 0xc50c06c2, 0x5a04abfc, 0x800bcadc, 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, 0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340, 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, + 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7, 0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, 0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068, 0xd4082471, 0x3320f46a, 0x43b7d4b7, + 0x500061af, 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, 0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45, 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504, 0x96eb27b3, 0x55fd3941, + 0xda2547e6, 0xabca0a9a, 0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb, 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, 0xaace1e7c, + 0xd3375fec, 0xce78a399, 0x406b2a42, 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b, 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, 0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb, + 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, 0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33, 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, + 0x9029317c, 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, 0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc, 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, 0x325f51eb, 0xd59bc0d1, + 0xf2bcc18f, 0x41113564, 0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b, 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115, 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, 0x85b2a20e, + 0xe6ba0d99, 0xde720c8c, 0x2da2f728, 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, 0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37, + 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d, 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, 0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b, 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, + 0xd9155ea3, 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, 0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d, 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c, 0x6a124237, 0xb79251e7, + 0x06a1bbe6, 0x4bfb6350, 0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9, 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, 0x9dbc8057, + 0xf0f7c086, 0x60787bf8, 0x6003604d, 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, 0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61, + 0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, 0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2, 0x466e598e, 0x20b45770, 0x8cd55591, + 0xc902de4c, 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, 0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633, 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, 0x1ab93d1d, 0x0ba5a4df, + 0xa186f20f, 0x2868f169, 0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52, 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027, 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, 0xf0177a28, + 0xc0f586e0, 0x006058aa, 0x30dc7d62, 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, 0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24, + 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc, 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, 0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c, 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, + 0xddc6c837, 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0, 0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, 0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe, 0xd5118e9d, 0xbf0f7315, + 0xd62d1c7e, 0xc700c47b, 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, 0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8, 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, 0x2939bbdb, + 0xa9ba4650, 0xac9526e8, 0xbe5ee304, 0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22, 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, + 0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9, 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59, 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, 0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, + 0x022b8b51, 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, 0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b, 0xe8d3c48d, 0x283b57cc, + 0xf8d56629, 0x79132e28, 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, 0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd, 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, 0x1b3f6d9b, + 0x1e6321f5, 0xf59c66fb, 0x26dcf319, 0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb, 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, + 0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32, 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, 0xb39a460a, 0x6445c0dd, 0x586cdecf, + 0x1c20c8ae, 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, 0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47, 0xd29be463, 0x542f5d9e, + 0xaec2771b, 0xf64e6370, 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, 0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84, 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048, 0x6f3f3b82, + 0x3520ab82, 0x011a1d4b, 0x277227f8, 0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd, 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, + 0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38, 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, 0xbf97222c, 0x15e6fc2a, 0x0f91fc71, + 0x9b941525, 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, 0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964, 0x9f1f9532, 0xe0d392df, + 0xd3a0342b, 0x8971f21e, 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, 0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d, 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, 0x1618b166, + 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, 0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02, 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, + 0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a, 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, 0x53113ec0, 0x1640e3d3, 0x38abbd60, + 0x2547adf0, 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060, 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, 0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9, 0x90d4f869, 0xa65cdea0, + 0x3f09252d, 0xc208e69f, 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6 }; + + // bcrypt IV: "OrpheanBeholderScryDoubt". The C implementation calls + // this "ciphertext", but it is really plaintext or an IV. We keep + // the name to make code comparison easier. + static private final int bf_crypt_ciphertext[] = { 0x4f727068, 0x65616e42, 0x65686f6c, 0x64657253, 0x63727944, 0x6f756274 }; + + // Table for Base64 encoding + static private final char base64_code[] = { '.', '/', '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' }; + + // Table for Base64 decoding + static private final byte index_64[] = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, 0, 1, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, -1, -1, -1, -1, -1, -1, -1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, -1, -1, -1, -1, -1, -1, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, -1, -1, -1, -1, -1 }; + + // Expanded Blowfish key + private int P[]; + private int S[]; + + /** + * Encode a byte array using bcrypt's slightly-modified base64 encoding scheme. Note that this is *not* compatible with the standard MIME-base64 encoding. + * + * @param d the byte array to encode + * @param len the number of bytes to encode + * @return base64-encoded string + * @exception IllegalArgumentException if the length is invalid + */ + private static String encode_base64(byte d[], int len) throws IllegalArgumentException { + int off = 0; + StringBuffer rs = new StringBuffer(); + int c1, c2; + + if (len <= 0 || len > d.length) + throw new IllegalArgumentException("Invalid len"); + + while (off < len) { + c1 = d[off++] & 0xff; + rs.append(base64_code[(c1 >> 2) & 0x3f]); + c1 = (c1 & 0x03) << 4; + if (off >= len) { + rs.append(base64_code[c1 & 0x3f]); + break; + } + c2 = d[off++] & 0xff; + c1 |= (c2 >> 4) & 0x0f; + rs.append(base64_code[c1 & 0x3f]); + c1 = (c2 & 0x0f) << 2; + if (off >= len) { + rs.append(base64_code[c1 & 0x3f]); + break; + } + c2 = d[off++] & 0xff; + c1 |= (c2 >> 6) & 0x03; + rs.append(base64_code[c1 & 0x3f]); + rs.append(base64_code[c2 & 0x3f]); + } + return rs.toString(); + } + + /** + * Look up the 3 bits base64-encoded by the specified character, range-checking againt conversion table + * + * @param x the base64-encoded value + * @return the decoded value of x + */ + private static byte char64(char x) { + if ((int) x < 0 || (int) x > index_64.length) + return -1; + return index_64[(int) x]; + } + + /** + * Decode a string encoded using bcrypt's base64 scheme to a byte array. Note that this is *not* compatible with the standard MIME-base64 encoding. + * + * @param s the string to decode + * @param maxolen the maximum number of bytes to decode + * @return an array containing the decoded bytes + * @throws IllegalArgumentException if maxolen is invalid + */ + private static byte[] decode_base64(String s, int maxolen) throws IllegalArgumentException { + StringBuffer rs = new StringBuffer(); + int off = 0, slen = s.length(), olen = 0; + byte ret[]; + byte c1, c2, c3, c4, o; + + if (maxolen <= 0) + throw new IllegalArgumentException("Invalid maxolen"); + + while (off < slen - 1 && olen < maxolen) { + c1 = char64(s.charAt(off++)); + c2 = char64(s.charAt(off++)); + if (c1 == -1 || c2 == -1) + break; + o = (byte) (c1 << 2); + o |= (c2 & 0x30) >> 4; + rs.append((char) o); + if (++olen >= maxolen || off >= slen) + break; + c3 = char64(s.charAt(off++)); + if (c3 == -1) + break; + o = (byte) ((c2 & 0x0f) << 4); + o |= (c3 & 0x3c) >> 2; + rs.append((char) o); + if (++olen >= maxolen || off >= slen) + break; + c4 = char64(s.charAt(off++)); + o = (byte) ((c3 & 0x03) << 6); + o |= c4; + rs.append((char) o); + ++olen; + } + + ret = new byte[olen]; + for (off = 0; off < olen; off++) + ret[off] = (byte) rs.charAt(off); + return ret; + } + + /** + * Blowfish encipher a single 64-bit block encoded as two 32-bit halves + * + * @param lr an array containing the two 32-bit half blocks + * @param off the position in the array of the blocks + */ + private final void encipher(int lr[], int off) { + int i, n, l = lr[off], r = lr[off + 1]; + + l ^= P[0]; + for (i = 0; i <= BLOWFISH_NUM_ROUNDS - 2;) { + // Feistel substitution on left word + n = S[(l >> 24) & 0xff]; + n += S[0x100 | ((l >> 16) & 0xff)]; + n ^= S[0x200 | ((l >> 8) & 0xff)]; + n += S[0x300 | (l & 0xff)]; + r ^= n ^ P[++i]; + + // Feistel substitution on right word + n = S[(r >> 24) & 0xff]; + n += S[0x100 | ((r >> 16) & 0xff)]; + n ^= S[0x200 | ((r >> 8) & 0xff)]; + n += S[0x300 | (r & 0xff)]; + l ^= n ^ P[++i]; + } + lr[off] = r ^ P[BLOWFISH_NUM_ROUNDS + 1]; + lr[off + 1] = l; + } + + /** + * Cycically extract a word of key material + * + * @param data the string to extract the data from + * @param offp a "pointer" (as a one-entry array) to the current offset into data + * @return the next word of material from data + */ + private static int streamtoword(byte data[], int offp[]) { + int i; + int word = 0; + int off = offp[0]; + + for (i = 0; i < 4; i++) { + word = (word << 8) | (data[off] & 0xff); + off = (off + 1) % data.length; + } + + offp[0] = off; + return word; + } + + /** + * Initialise the Blowfish key schedule + */ + private void init_key() { + P = (int[]) P_orig.clone(); + S = (int[]) S_orig.clone(); + } + + /** + * Key the Blowfish cipher + * + * @param key an array containing the key + */ + private void key(byte key[]) { + int i; + int koffp[] = { 0 }; + int lr[] = { 0, 0 }; + int plen = P.length, slen = S.length; + + for (i = 0; i < plen; i++) + P[i] = P[i] ^ streamtoword(key, koffp); + + for (i = 0; i < plen; i += 2) { + encipher(lr, 0); + P[i] = lr[0]; + P[i + 1] = lr[1]; + } + + for (i = 0; i < slen; i += 2) { + encipher(lr, 0); + S[i] = lr[0]; + S[i + 1] = lr[1]; + } + } + + /** + * Perform the "enhanced key schedule" step described by Provos and Mazieres in "A Future-Adaptable Password Scheme" http://www.openbsd.org/papers/bcrypt-paper.ps + * + * @param data salt information + * @param key password information + */ + private void ekskey(byte data[], byte key[]) { + int i; + int koffp[] = { 0 }, doffp[] = { 0 }; + int lr[] = { 0, 0 }; + int plen = P.length, slen = S.length; + + for (i = 0; i < plen; i++) + P[i] = P[i] ^ streamtoword(key, koffp); + + for (i = 0; i < plen; i += 2) { + lr[0] ^= streamtoword(data, doffp); + lr[1] ^= streamtoword(data, doffp); + encipher(lr, 0); + P[i] = lr[0]; + P[i + 1] = lr[1]; + } + + for (i = 0; i < slen; i += 2) { + lr[0] ^= streamtoword(data, doffp); + lr[1] ^= streamtoword(data, doffp); + encipher(lr, 0); + S[i] = lr[0]; + S[i + 1] = lr[1]; + } + } + + /** + * 加密密文 + * + * @param password 明文密码 + * @param salt 加盐 + * @param log_rounds hash中叠加的对数 + * @param cdata 加密数据 + * @return 加密后的密文 + */ + public byte[] crypt(byte password[], byte salt[], int log_rounds, int cdata[]) { + int rounds, i, j; + int clen = cdata.length; + byte ret[]; + + if (log_rounds < 4 || log_rounds > 30) + throw new IllegalArgumentException("Bad number of rounds"); + rounds = 1 << log_rounds; + if (salt.length != BCRYPT_SALT_LEN) + throw new IllegalArgumentException("Bad salt length"); + + init_key(); + ekskey(salt, password); + for (i = 0; i != rounds; i++) { + key(password); + key(salt); + } + + for (i = 0; i < 64; i++) { + for (j = 0; j < (clen >> 1); j++) + encipher(cdata, j << 1); + } + + ret = new byte[clen * 4]; + for (i = 0, j = 0; i < clen; i++) { + ret[j++] = (byte) ((cdata[i] >> 24) & 0xff); + ret[j++] = (byte) ((cdata[i] >> 16) & 0xff); + ret[j++] = (byte) ((cdata[i] >> 8) & 0xff); + ret[j++] = (byte) (cdata[i] & 0xff); + } + return ret; + } + + /** + * 生成密文,使用长度为10的加盐方式 + * + * @param password 需要加密的明文 + * @return 密文 + */ + public static String hashpw(String password) { + return hashpw(password, gensalt()); + } + + /** + * 生成密文 + * + * @param password 需要加密的明文 + * @param salt 盐,使用{@link #gensalt()} 生成 + * @return 密文 + */ + public static String hashpw(String password, String salt) { + BCrypt bcrypt; + String real_salt; + byte saltb[], hashed[]; + char minor = (char) 0; + int rounds, off = 0; + StringBuilder rs = new StringBuilder(); + + if (salt.charAt(0) != '$' || salt.charAt(1) != '2') + throw new IllegalArgumentException("Invalid salt version"); + if (salt.charAt(2) == '$') + off = 3; + else { + minor = salt.charAt(2); + if (minor != 'a' || salt.charAt(3) != '$') + throw new IllegalArgumentException("Invalid salt revision"); + off = 4; + } + + // Extract number of rounds + if (salt.charAt(off + 2) > '$') + throw new IllegalArgumentException("Missing salt rounds"); + rounds = Integer.parseInt(salt.substring(off, off + 2)); + + real_salt = salt.substring(off + 3, off + 25); + byte[] passwordb = (password + (minor >= 'a' ? "\000" : "")).getBytes(CharsetUtil.CHARSET_UTF_8); + saltb = decode_base64(real_salt, BCRYPT_SALT_LEN); + + bcrypt = new BCrypt(); + hashed = bcrypt.crypt(passwordb, saltb, rounds, (int[]) bf_crypt_ciphertext.clone()); + + rs.append("$2"); + if (minor >= 'a') + rs.append(minor); + rs.append("$"); + if (rounds < 10) + rs.append("0"); + if (rounds > 30) { + throw new IllegalArgumentException("rounds exceeds maximum (30)"); + } + rs.append(Integer.toString(rounds)); + rs.append("$"); + rs.append(encode_base64(saltb, saltb.length)); + rs.append(encode_base64(hashed, bf_crypt_ciphertext.length * 4 - 1)); + return rs.toString(); + } + + /** + * 生成盐 + * + * @param log_rounds hash中叠加的2的对数 - the work factor therefore increases as 2**log_rounds. + * @param random {@link SecureRandom} + * @return an encoded salt value + */ + public static String gensalt(int log_rounds, SecureRandom random) { + final StringBuilder rs = new StringBuilder(); + byte rnd[] = new byte[BCRYPT_SALT_LEN]; + + random.nextBytes(rnd); + + rs.append("$2a$"); + if (log_rounds < 10) + rs.append("0"); + if (log_rounds > 30) { + throw new IllegalArgumentException("log_rounds exceeds maximum (30)"); + } + rs.append(Integer.toString(log_rounds)); + rs.append("$"); + rs.append(encode_base64(rnd, rnd.length)); + return rs.toString(); + } + + /** + * 生成盐 + * + * @param log_rounds the log2 of the number of rounds of hashing to apply - the work factor therefore increases as 2**log_rounds. + * @return 盐 + */ + public static String gensalt(int log_rounds) { + return gensalt(log_rounds, new SecureRandom()); + } + + /** + * 生成盐 + * + * @return 盐 + */ + public static String gensalt() { + return gensalt(GENSALT_DEFAULT_LOG2_ROUNDS); + } + + /** + * 检查明文密码文本是否匹配加密后的文本 + * + * @param plaintext 需要验证的明文密码 + * @param hashed 密文 + * @return 是否匹配 + */ + public static boolean checkpw(String plaintext, String hashed) { + byte hashed_bytes[]; + byte try_bytes[]; + String try_pw = hashpw(plaintext, hashed); + hashed_bytes = hashed.getBytes(CharsetUtil.CHARSET_UTF_8); + try_bytes = try_pw.getBytes(CharsetUtil.CHARSET_UTF_8); + if (hashed_bytes.length != try_bytes.length) { + return false; + } + byte ret = 0; + for (int i = 0; i < try_bytes.length; i++) + ret |= hashed_bytes[i] ^ try_bytes[i]; + return ret == 0; + } +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/digest/DigestAlgorithm.java b/hutool-crypto/src/main/java/cn/hutool/crypto/digest/DigestAlgorithm.java new file mode 100644 index 000000000..2283af731 --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/digest/DigestAlgorithm.java @@ -0,0 +1,35 @@ +package cn.hutool.crypto.digest; + +/** + * 摘要算法类型
+ * see: https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#MessageDigest + * + * @author Looly + */ +public enum DigestAlgorithm { + MD2("MD2"), + MD5("MD5"), + SHA1("SHA-1"), + SHA256("SHA-256"), + SHA384("SHA-384"), + SHA512("SHA-512"); + + private String value; + + /** + * 构造 + * + * @param value 算法字符串表示 + */ + private DigestAlgorithm(String value) { + this.value = value; + } + + /** + * 获取算法字符串表示 + * @return 算法字符串表示 + */ + public String getValue() { + return this.value; + } +} \ No newline at end of file diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/digest/DigestUtil.java b/hutool-crypto/src/main/java/cn/hutool/crypto/digest/DigestUtil.java new file mode 100644 index 000000000..96bcce96e --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/digest/DigestUtil.java @@ -0,0 +1,486 @@ +package cn.hutool.crypto.digest; + +import java.io.File; +import java.io.InputStream; +import java.nio.charset.Charset; + +import javax.crypto.SecretKey; + +import cn.hutool.core.util.CharsetUtil; + +/** + * 摘要算法工具类 + * + * @author Looly + * + */ +public class DigestUtil { + + // ------------------------------------------------------------------------------------------- MD5 + /** + * 计算32位MD5摘要值 + * + * @param data 被摘要数据 + * @return MD5摘要 + */ + public static byte[] md5(byte[] data) { + return new MD5().digest(data); + } + + /** + * 计算32位MD5摘要值 + * + * @param data 被摘要数据 + * @param charset 编码 + * @return MD5摘要 + */ + public static byte[] md5(String data, String charset) { + return new MD5().digest(data, charset); + } + + /** + * 计算32位MD5摘要值,使用UTF-8编码 + * + * @param data 被摘要数据 + * @return MD5摘要 + */ + public static byte[] md5(String data) { + return md5(data, CharsetUtil.UTF_8); + } + + /** + * 计算32位MD5摘要值 + * + * @param data 被摘要数据 + * @return MD5摘要 + */ + public static byte[] md5(InputStream data) { + return new MD5().digest(data); + } + + /** + * 计算32位MD5摘要值 + * + * @param file 被摘要文件 + * @return MD5摘要 + */ + public static byte[] md5(File file) { + return new MD5().digest(file); + } + + /** + * 计算32位MD5摘要值,并转为16进制字符串 + * + * @param data 被摘要数据 + * @return MD5摘要的16进制表示 + */ + public static String md5Hex(byte[] data) { + return new MD5().digestHex(data); + } + + /** + * 计算32位MD5摘要值,并转为16进制字符串 + * + * @param data 被摘要数据 + * @param charset 编码 + * @return MD5摘要的16进制表示 + */ + public static String md5Hex(String data, String charset) { + return new MD5().digestHex(data, charset); + } + + /** + * 计算32位MD5摘要值,并转为16进制字符串 + * + * @param data 被摘要数据 + * @param charset 编码 + * @return MD5摘要的16进制表示 + * @since 4.6.0 + */ + public static String md5Hex(String data, Charset charset) { + return new MD5().digestHex(data, charset); + } + + /** + * 计算32位MD5摘要值,并转为16进制字符串 + * + * @param data 被摘要数据 + * @return MD5摘要的16进制表示 + */ + public static String md5Hex(String data) { + return md5Hex(data, CharsetUtil.UTF_8); + } + + /** + * 计算32位MD5摘要值,并转为16进制字符串 + * + * @param data 被摘要数据 + * @return MD5摘要的16进制表示 + */ + public static String md5Hex(InputStream data) { + return new MD5().digestHex(data); + } + + /** + * 计算32位MD5摘要值,并转为16进制字符串 + * + * @param file 被摘要文件 + * @return MD5摘要的16进制表示 + */ + public static String md5Hex(File file) { + return new MD5().digestHex(file); + } + + // ------------------------------------------------------------------------------------------- MD5 16 + /** + * 计算16位MD5摘要值,并转为16进制字符串 + * + * @param data 被摘要数据 + * @return MD5摘要的16进制表示 + * @since 4.6.0 + */ + public static String md5Hex16(byte[] data) { + return new MD5().digestHex16(data); + } + + /** + * 计算16位MD5摘要值,并转为16进制字符串 + * + * @param data 被摘要数据 + * @param charset 编码 + * @return MD5摘要的16进制表示 + * @since 4.6.0 + */ + public static String md5Hex16(String data, Charset charset) { + return new MD5().digestHex16(data, charset); + } + + /** + * 计算16位MD5摘要值,并转为16进制字符串 + * + * @param data 被摘要数据 + * @return MD5摘要的16进制表示 + * @since 4.6.0 + */ + public static String md5Hex16(String data) { + return md5Hex16(data, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 计算16位MD5摘要值,并转为16进制字符串 + * + * @param data 被摘要数据 + * @return MD5摘要的16进制表示 + * @since 4.6.0 + */ + public static String md5Hex16(InputStream data) { + return new MD5().digestHex16(data); + } + + /** + * 计算16位MD5摘要值,并转为16进制字符串 + * + * @param file 被摘要文件 + * @return MD5摘要的16进制表示 + * @since 4.6.0 + */ + public static String md5Hex16(File file) { + return new MD5().digestHex16(file); + } + + /** + * 32位MD5转16位MD5 + * + * @param md5Hex 32位MD5 + * @return 16位MD5 + * @since 4.4.1 + */ + public static String md5HexTo16(String md5Hex) { + return md5Hex.substring(8, 24); + } + + // ------------------------------------------------------------------------------------------- SHA-1 + /** + * 计算SHA-1摘要值 + * + * @param data 被摘要数据 + * @return SHA-1摘要 + */ + public static byte[] sha1(byte[] data) { + return new Digester(DigestAlgorithm.SHA1).digest(data); + } + + /** + * 计算SHA-1摘要值 + * + * @param data 被摘要数据 + * @param charset 编码 + * @return SHA-1摘要 + */ + public static byte[] sha1(String data, String charset) { + return new Digester(DigestAlgorithm.SHA1).digest(data, charset); + } + + /** + * 计算sha1摘要值,使用UTF-8编码 + * + * @param data 被摘要数据 + * @return MD5摘要 + */ + public static byte[] sha1(String data) { + return sha1(data, CharsetUtil.UTF_8); + } + + /** + * 计算SHA-1摘要值 + * + * @param data 被摘要数据 + * @return SHA-1摘要 + */ + public static byte[] sha1(InputStream data) { + return new Digester(DigestAlgorithm.SHA1).digest(data); + } + + /** + * 计算SHA-1摘要值 + * + * @param file 被摘要文件 + * @return SHA-1摘要 + */ + public static byte[] sha1(File file) { + return new Digester(DigestAlgorithm.SHA1).digest(file); + } + + /** + * 计算SHA-1摘要值,并转为16进制字符串 + * + * @param data 被摘要数据 + * @return SHA-1摘要的16进制表示 + */ + public static String sha1Hex(byte[] data) { + return new Digester(DigestAlgorithm.SHA1).digestHex(data); + } + + /** + * 计算SHA-1摘要值,并转为16进制字符串 + * + * @param data 被摘要数据 + * @param charset 编码 + * @return SHA-1摘要的16进制表示 + */ + public static String sha1Hex(String data, String charset) { + return new Digester(DigestAlgorithm.SHA1).digestHex(data, charset); + } + + /** + * 计算SHA-1摘要值,并转为16进制字符串 + * + * @param data 被摘要数据 + * @return SHA-1摘要的16进制表示 + */ + public static String sha1Hex(String data) { + return sha1Hex(data, CharsetUtil.UTF_8); + } + + /** + * 计算SHA-1摘要值,并转为16进制字符串 + * + * @param data 被摘要数据 + * @return SHA-1摘要的16进制表示 + */ + public static String sha1Hex(InputStream data) { + return new Digester(DigestAlgorithm.SHA1).digestHex(data); + } + + /** + * 计算SHA-1摘要值,并转为16进制字符串 + * + * @param file 被摘要文件 + * @return SHA-1摘要的16进制表示 + */ + public static String sha1Hex(File file) { + return new Digester(DigestAlgorithm.SHA1).digestHex(file); + } + + // ------------------------------------------------------------------------------------------- SHA-256 + /** + * 计算SHA-256摘要值 + * + * @param data 被摘要数据 + * @return SHA-256摘要 + * @since 3.0.8 + */ + public static byte[] sha256(byte[] data) { + return new Digester(DigestAlgorithm.SHA256).digest(data); + } + + /** + * 计算SHA-256摘要值 + * + * @param data 被摘要数据 + * @param charset 编码 + * @return SHA-256摘要 + * @since 3.0.8 + */ + public static byte[] sha256(String data, String charset) { + return new Digester(DigestAlgorithm.SHA256).digest(data, charset); + } + + /** + * 计算sha256摘要值,使用UTF-8编码 + * + * @param data 被摘要数据 + * @return MD5摘要 + * @since 3.0.8 + */ + public static byte[] sha256(String data) { + return sha256(data, CharsetUtil.UTF_8); + } + + /** + * 计算SHA-256摘要值 + * + * @param data 被摘要数据 + * @return SHA-256摘要 + * @since 3.0.8 + */ + public static byte[] sha256(InputStream data) { + return new Digester(DigestAlgorithm.SHA256).digest(data); + } + + /** + * 计算SHA-256摘要值 + * + * @param file 被摘要文件 + * @return SHA-256摘要 + * @since 3.0.8 + */ + public static byte[] sha256(File file) { + return new Digester(DigestAlgorithm.SHA256).digest(file); + } + + /** + * 计算SHA-1摘要值,并转为16进制字符串 + * + * @param data 被摘要数据 + * @return SHA-256摘要的16进制表示 + * @since 3.0.8 + */ + public static String sha256Hex(byte[] data) { + return new Digester(DigestAlgorithm.SHA256).digestHex(data); + } + + /** + * 计算SHA-256摘要值,并转为16进制字符串 + * + * @param data 被摘要数据 + * @param charset 编码 + * @return SHA-256摘要的16进制表示 + * @since 3.0.8 + */ + public static String sha256Hex(String data, String charset) { + return new Digester(DigestAlgorithm.SHA256).digestHex(data, charset); + } + + /** + * 计算SHA-256摘要值,并转为16进制字符串 + * + * @param data 被摘要数据 + * @return SHA-256摘要的16进制表示 + * @since 3.0.8 + */ + public static String sha256Hex(String data) { + return sha256Hex(data, CharsetUtil.UTF_8); + } + + /** + * 计算SHA-256摘要值,并转为16进制字符串 + * + * @param data 被摘要数据 + * @return SHA-256摘要的16进制表示 + * @since 3.0.8 + */ + public static String sha256Hex(InputStream data) { + return new Digester(DigestAlgorithm.SHA256).digestHex(data); + } + + /** + * 计算SHA-256摘要值,并转为16进制字符串 + * + * @param file 被摘要文件 + * @return SHA-256摘要的16进制表示 + * @since 3.0.8 + */ + public static String sha256Hex(File file) { + return new Digester(DigestAlgorithm.SHA256).digestHex(file); + } + + // ------------------------------------------------------------------------------------------- Hmac + /** + * 创建HMac对象,调用digest方法可获得hmac值 + * + * @param algorithm {@link HmacAlgorithm} + * @param key 密钥,如果为null生成随机密钥 + * @return {@link HMac} + * @since 3.0.3 + */ + public static HMac hmac(HmacAlgorithm algorithm, byte[] key) { + return new HMac(algorithm, key); + } + + /** + * 创建HMac对象,调用digest方法可获得hmac值 + * + * @param algorithm {@link HmacAlgorithm} + * @param key 密钥{@link SecretKey},如果为null生成随机密钥 + * @return {@link HMac} + * @since 3.0.3 + */ + public static HMac hmac(HmacAlgorithm algorithm, SecretKey key) { + return new HMac(algorithm, key); + } + + /** + * 新建摘要器 + * + * @param algorithm 签名算法 + * @return Digester + * @since 4.0.1 + */ + public static Digester digester(DigestAlgorithm algorithm) { + return new Digester(algorithm); + } + + /** + * 新建摘要器 + * + * @param algorithm 签名算法 + * @return Digester + * @since 4.2.1 + */ + public static Digester digester(String algorithm) { + return new Digester(algorithm); + } + + /** + * 生成Bcrypt加密后的密文 + * + * @param password 明文密码 + * @return 加密后的密文 + * @since 4.1.1 + */ + public static String bcrypt(String password) { + return BCrypt.hashpw(password); + } + + /** + * 验证密码是否与Bcrypt加密后的密文匹配 + * + * @param password 明文密码 + * @return 是否匹配 + * @since 4.1.1 + */ + public static boolean bcryptCheck(String password, String hashed) { + return BCrypt.checkpw(password, hashed); + } +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/digest/Digester.java b/hutool-crypto/src/main/java/cn/hutool/crypto/digest/Digester.java new file mode 100644 index 000000000..d26c6a9eb --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/digest/Digester.java @@ -0,0 +1,462 @@ +package cn.hutool.crypto.digest; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.HexUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.CryptoException; +import cn.hutool.crypto.SecureUtil; + +/** + * 摘要算法
+ * 注意:此对象实例化后为非线程安全! + * + * @author Looly + * + */ +public class Digester { + + private MessageDigest digest; + /** 盐值 */ + protected byte[] salt; + /** 加盐位置,既将盐值字符串放置在数据的index数,默认0 */ + protected int saltPosition; + /** 散列次数 */ + protected int digestCount; + + // ------------------------------------------------------------------------------------------- Constructor start + /** + * 构造 + * + * @param algorithm 算法枚举 + */ + public Digester(DigestAlgorithm algorithm) { + this(algorithm.getValue()); + } + + /** + * 构造 + * + * @param algorithm 算法枚举 + */ + public Digester(String algorithm) { + this(algorithm, null); + } + + /** + * 构造 + * + * @param algorithm 算法 + * @param provider 算法提供者,null表示JDK默认,可以引入Bouncy Castle等来提供更多算法支持 + * @since 4.5.1 + */ + public Digester(DigestAlgorithm algorithm, Provider provider) { + init(algorithm.getValue(), provider); + } + + /** + * 构造 + * + * @param algorithm 算法 + * @param provider 算法提供者,null表示JDK默认,可以引入Bouncy Castle等来提供更多算法支持 + * @since 4.5.1 + */ + public Digester(String algorithm, Provider provider) { + init(algorithm, provider); + } + // ------------------------------------------------------------------------------------------- Constructor end + + /** + * 初始化 + * + * @param algorithm 算法 + * @param provider 算法提供者,null表示JDK默认,可以引入Bouncy Castle等来提供更多算法支持 + * @return {@link Digester} + * @throws CryptoException Cause by IOException + */ + public Digester init(String algorithm, Provider provider) { + if(null == provider) { + this.digest = SecureUtil.createMessageDigest(algorithm); + }else { + try { + this.digest = MessageDigest.getInstance(algorithm, provider); + } catch (NoSuchAlgorithmException e) { + throw new CryptoException(e); + } + } + return this; + } + + /** + * 设置加盐内容 + * + * @param salt 盐值 + * @return this + * @since 4.4.3 + */ + public Digester setSalt(byte[] salt) { + this.salt = salt; + return this; + } + + /** + * 设置加盐的位置,只有盐值存在时有效
+ * 加盐的位置指盐位于数据byte数组中的位置,例如: + * + *

+	 * data: 0123456
+	 * 
+ * + * 则当saltPosition = 2时,盐位于data的1和2中间,既第二个空隙,既: + * + *
+	 * data: 01[salt]23456
+	 * 
+ * + * + * @param saltPosition 盐的位置 + * @return this + * @since 4.4.3 + */ + public Digester setSaltPosition(int saltPosition) { + this.saltPosition = saltPosition; + return this; + } + + /** + * 设置重复计算摘要值次数 + * + * @param digestCount 摘要值次数 + * @return this + */ + public Digester setDigestCount(int digestCount) { + this.digestCount = digestCount; + return this; + } + + /** + * 重置{@link MessageDigest} + * + * @return this + * @since 4.5.1 + */ + public Digester reset() { + this.digest.reset(); + return this; + } + + // ------------------------------------------------------------------------------------------- Digest + /** + * 生成文件摘要 + * + * @param data 被摘要数据 + * @param charsetName 编码 + * @return 摘要 + */ + public byte[] digest(String data, String charsetName) { + return digest(data, CharsetUtil.charset(charsetName)); + } + + /** + * 生成文件摘要 + * + * @param data 被摘要数据 + * @param charset 编码 + * @return 摘要 + * @since 4.6.0 + */ + public byte[] digest(String data, Charset charset) { + return digest(StrUtil.bytes(data, charset)); + } + + /** + * 生成文件摘要 + * + * @param data 被摘要数据 + * @return 摘要 + */ + public byte[] digest(String data) { + return digest(data, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 生成文件摘要,并转为16进制字符串 + * + * @param data 被摘要数据 + * @param charsetName 编码 + * @return 摘要 + */ + public String digestHex(String data, String charsetName) { + return digestHex(data, CharsetUtil.charset(charsetName)); + } + + /** + * 生成文件摘要,并转为16进制字符串 + * + * @param data 被摘要数据 + * @param charset 编码 + * @return 摘要 + * @since 4.6.0 + */ + public String digestHex(String data, Charset charset) { + return HexUtil.encodeHexStr(digest(data, charset)); + } + + /** + * 生成文件摘要 + * + * @param data 被摘要数据 + * @return 摘要 + */ + public String digestHex(String data) { + return digestHex(data, CharsetUtil.UTF_8); + } + + /** + * 生成文件摘要
+ * 使用默认缓存大小,见 {@link IoUtil#DEFAULT_BUFFER_SIZE} + * + * @param file 被摘要文件 + * @return 摘要bytes + * @throws CryptoException Cause by IOException + */ + public byte[] digest(File file) throws CryptoException { + InputStream in = null; + try { + in = FileUtil.getInputStream(file); + return digest(in); + } finally { + IoUtil.close(in); + } + } + + /** + * 生成文件摘要,并转为16进制字符串
+ * 使用默认缓存大小,见 {@link IoUtil#DEFAULT_BUFFER_SIZE} + * + * @param file 被摘要文件 + * @return 摘要 + */ + public String digestHex(File file) { + return HexUtil.encodeHexStr(digest(file)); + } + + /** + * 生成摘要,考虑加盐和重复摘要次数 + * + * @param data 数据bytes + * @return 摘要bytes + */ + public byte[] digest(byte[] data) { + byte[] result = null; + if (this.saltPosition <= 0) { + // 加盐在开头,自动忽略空盐值 + result = doDigest(this.salt, data); + } else if (this.saltPosition >= data.length) { + // 加盐在末尾,自动忽略空盐值 + result = doDigest(data, this.salt); + } else if (ArrayUtil.isNotEmpty(this.salt)) { + // 加盐在中间 + this.digest.update(data, 0, this.saltPosition); + this.digest.update(this.salt); + this.digest.update(data, this.saltPosition, data.length - this.saltPosition); + result = this.digest.digest(); + } else { + // 无加盐 + result = doDigest(data); + } + + return resetAndRepeatDigest(result); + } + + /** + * 生成摘要,并转为16进制字符串
+ * + * @param data 被摘要数据 + * @return 摘要 + */ + public String digestHex(byte[] data) { + return HexUtil.encodeHexStr(digest(data)); + } + + /** + * 生成摘要,使用默认缓存大小,见 {@link IoUtil#DEFAULT_BUFFER_SIZE} + * + * @param data {@link InputStream} 数据流 + * @return 摘要bytes + */ + public byte[] digest(InputStream data) { + return digest(data, IoUtil.DEFAULT_BUFFER_SIZE); + } + + /** + * 生成摘要,并转为16进制字符串
+ * 使用默认缓存大小,见 {@link IoUtil#DEFAULT_BUFFER_SIZE} + * + * @param data 被摘要数据 + * @return 摘要 + */ + public String digestHex(InputStream data) { + return HexUtil.encodeHexStr(digest(data)); + } + + /** + * 生成摘要 + * + * @param data {@link InputStream} 数据流 + * @param bufferLength 缓存长度,不足1使用 {@link IoUtil#DEFAULT_BUFFER_SIZE} 做为默认值 + * @return 摘要bytes + * @throws IORuntimeException IO异常 + */ + public byte[] digest(InputStream data, int bufferLength) throws IORuntimeException { + if (bufferLength < 1) { + bufferLength = IoUtil.DEFAULT_BUFFER_SIZE; + } + + byte[] result; + try { + if (ArrayUtil.isEmpty(this.salt)) { + result = digestWithoutSalt(data, bufferLength); + } else { + result = digestWithSalt(data, bufferLength); + } + } catch (IOException e) { + throw new IORuntimeException(e); + } + + return resetAndRepeatDigest(result); + } + + /** + * 生成摘要,并转为16进制字符串
+ * 使用默认缓存大小,见 {@link IoUtil#DEFAULT_BUFFER_SIZE} + * + * @param data 被摘要数据 + * @param bufferLength 缓存长度,不足1使用 {@link IoUtil#DEFAULT_BUFFER_SIZE} 做为默认值 + * @return 摘要 + */ + public String digestHex(InputStream data, int bufferLength) { + return HexUtil.encodeHexStr(digest(data, bufferLength)); + } + + /** + * 获得 {@link MessageDigest} + * + * @return {@link MessageDigest} + */ + public MessageDigest getDigest() { + return digest; + } + + /** + * 获取散列长度,0表示不支持此方法 + * + * @return 散列长度,0表示不支持此方法 + * @since 4.5.0 + */ + public int getDigestLength() { + return this.digest.getDigestLength(); + } + + // -------------------------------------------------------------------------------- Private method start + /** + * 生成摘要 + * + * @param data {@link InputStream} 数据流 + * @param bufferLength 缓存长度,不足1使用 {@link IoUtil#DEFAULT_BUFFER_SIZE} 做为默认值 + * @return 摘要bytes + * @throws IOException 从流中读取数据引发的IO异常 + */ + private byte[] digestWithoutSalt(InputStream data, int bufferLength) throws IOException { + final byte[] buffer = new byte[bufferLength]; + int read; + while ((read = data.read(buffer, 0, bufferLength)) > -1) { + this.digest.update(buffer, 0, read); + } + return this.digest.digest(); + } + + /** + * 生成摘要 + * + * @param data {@link InputStream} 数据流 + * @param bufferLength 缓存长度,不足1使用 {@link IoUtil#DEFAULT_BUFFER_SIZE} 做为默认值 + * @return 摘要bytes + * @throws IOException 从流中读取数据引发的IO异常 + */ + private byte[] digestWithSalt(InputStream data, int bufferLength) throws IOException { + if (this.saltPosition <= 0) { + // 加盐在开头 + this.digest.update(this.salt); + } + + final byte[] buffer = new byte[bufferLength]; + int total = 0; + int read; + while ((read = data.read(buffer, 0, bufferLength)) > -1) { + total += read; + if (this.saltPosition > 0 && total >= this.saltPosition) { + if (total != this.saltPosition) { + digest.update(buffer, 0, total - this.saltPosition); + } + // 加盐在中间 + this.digest.update(this.salt); + this.digest.update(buffer, total - this.saltPosition, read); + } else { + this.digest.update(buffer, 0, read); + } + } + + if (total < this.saltPosition) { + // 加盐在末尾 + this.digest.update(this.salt); + } + + return this.digest.digest(); + } + + /** + * 生成摘要 + * + * @param datas 数据bytes + * @return 摘要bytes + * @since 4.4.3 + */ + private byte[] doDigest(byte[]... datas) { + for (byte[] data : datas) { + if (null != data) { + this.digest.update(data); + } + } + return this.digest.digest(); + } + + /** + * 重复计算摘要,取决于{@link #digestCount} 值
+ * 每次计算摘要前都会重置{@link #digest} + * + * @param digestData 第一次摘要过的数据 + * @return 摘要 + */ + private byte[] resetAndRepeatDigest(byte[] digestData) { + final int digestCount = Math.max(1, this.digestCount); + reset(); + for (int i = 0; i < digestCount - 1; i++) { + digestData = doDigest(digestData); + reset(); + } + return digestData; + } + // -------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/digest/HMac.java b/hutool-crypto/src/main/java/cn/hutool/crypto/digest/HMac.java new file mode 100644 index 000000000..310cd3d5c --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/digest/HMac.java @@ -0,0 +1,226 @@ +package cn.hutool.crypto.digest; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.InputStream; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.HexUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.CryptoException; +import cn.hutool.crypto.digest.mac.MacEngine; +import cn.hutool.crypto.digest.mac.MacEngineFactory; + +/** + * HMAC摘要算法
+ * HMAC,全称为“Hash Message Authentication Code”,中文名“散列消息鉴别码”
+ * 主要是利用哈希算法,以一个密钥和一个消息为输入,生成一个消息摘要作为输出。
+ * 一般的,消息鉴别码用于验证传输于两个共 同享有一个密钥的单位之间的消息。
+ * HMAC 可以与任何迭代散列函数捆绑使用。MD5 和 SHA-1 就是这种散列函数。HMAC 还可以使用一个用于计算和确认消息鉴别值的密钥。
+ * 注意:此对象实例化后为非线程安全! + * @author Looly + * + */ +public class HMac { + + private MacEngine engine; + + // ------------------------------------------------------------------------------------------- Constructor start + /** + * 构造,自动生成密钥 + * @param algorithm 算法 {@link HmacAlgorithm} + */ + public HMac(HmacAlgorithm algorithm) { + this(algorithm, (SecretKey)null); + } + + /** + * 构造 + * @param algorithm 算法 {@link HmacAlgorithm} + * @param key 密钥 + */ + public HMac(HmacAlgorithm algorithm, byte[] key) { + this(algorithm.getValue(), key); + } + + /** + * 构造 + * @param algorithm 算法 {@link HmacAlgorithm} + * @param key 密钥 + */ + public HMac(HmacAlgorithm algorithm, SecretKey key) { + this(algorithm.getValue(), key); + } + + /** + * 构造 + * @param algorithm 算法 + * @param key 密钥 + * @since 4.5.13 + */ + public HMac(String algorithm, byte[] key) { + this(algorithm, new SecretKeySpec(key, algorithm)); + } + + /** + * 构造 + * @param algorithm 算法 + * @param key 密钥 + * @since 4.5.13 + */ + public HMac(String algorithm, SecretKey key) { + this(MacEngineFactory.createEngine(algorithm, key)); + } + + /** + * 构造 + * @param engine MAC算法实现引擎 + * @since 4.5.13 + */ + public HMac(MacEngine engine) { + this.engine = engine; + } + // ------------------------------------------------------------------------------------------- Constructor end + + // ------------------------------------------------------------------------------------------- Digest + /** + * 生成文件摘要 + * + * @param data 被摘要数据 + * @param charset 编码 + * @return 摘要 + */ + public byte[] digest(String data, String charset) { + return digest(StrUtil.bytes(data, charset)); + } + + /** + * 生成文件摘要 + * + * @param data 被摘要数据 + * @return 摘要 + */ + public byte[] digest(String data) { + return digest(data, CharsetUtil.UTF_8); + } + + /** + * 生成文件摘要,并转为16进制字符串 + * + * @param data 被摘要数据 + * @param charset 编码 + * @return 摘要 + */ + public String digestHex(String data, String charset) { + return HexUtil.encodeHexStr(digest(data, charset)); + } + + /** + * 生成文件摘要 + * + * @param data 被摘要数据 + * @return 摘要 + */ + public String digestHex(String data) { + return digestHex(data, CharsetUtil.UTF_8); + } + + /** + * 生成文件摘要
+ * 使用默认缓存大小,见 {@link IoUtil#DEFAULT_BUFFER_SIZE} + * + * @param file 被摘要文件 + * @return 摘要bytes + * @throws CryptoException Cause by IOException + */ + public byte[] digest(File file) throws CryptoException{ + InputStream in = null; + try { + in = FileUtil.getInputStream(file); + return digest(in); + } finally{ + IoUtil.close(in); + } + } + + /** + * 生成文件摘要,并转为16进制字符串
+ * 使用默认缓存大小,见 {@link IoUtil#DEFAULT_BUFFER_SIZE} + * + * @param file 被摘要文件 + * @return 摘要 + */ + public String digestHex(File file) { + return HexUtil.encodeHexStr(digest(file)); + } + + /** + * 生成摘要 + * + * @param data 数据bytes + * @return 摘要bytes + */ + public byte[] digest(byte[] data) { + return digest(new ByteArrayInputStream(data), -1); + } + + /** + * 生成摘要,并转为16进制字符串
+ * + * @param data 被摘要数据 + * @return 摘要 + */ + public String digestHex(byte[] data) { + return HexUtil.encodeHexStr(digest(data)); + } + + /** + * 生成摘要,使用默认缓存大小,见 {@link IoUtil#DEFAULT_BUFFER_SIZE} + * + * @param data {@link InputStream} 数据流 + * @return 摘要bytes + */ + public byte[] digest(InputStream data) { + return digest(data, IoUtil.DEFAULT_BUFFER_SIZE); + } + + /** + * 生成摘要,并转为16进制字符串
+ * 使用默认缓存大小,见 {@link IoUtil#DEFAULT_BUFFER_SIZE} + * + * @param data 被摘要数据 + * @return 摘要 + */ + public String digestHex(InputStream data) { + return HexUtil.encodeHexStr(digest(data)); + } + + /** + * 生成摘要 + * + * @param data {@link InputStream} 数据流 + * @param bufferLength 缓存长度,不足1使用 {@link IoUtil#DEFAULT_BUFFER_SIZE} 做为默认值 + * @return 摘要bytes + */ + public byte[] digest(InputStream data, int bufferLength) { + return this.engine.digest(data, bufferLength); + } + + /** + * 生成摘要,并转为16进制字符串
+ * 使用默认缓存大小,见 {@link IoUtil#DEFAULT_BUFFER_SIZE} + * + * @param data 被摘要数据 + * @param bufferLength 缓存长度,不足1使用 {@link IoUtil#DEFAULT_BUFFER_SIZE} 做为默认值 + * @return 摘要 + */ + public String digestHex(InputStream data, int bufferLength) { + return HexUtil.encodeHexStr(digest(data, bufferLength)); + } + +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/digest/HmacAlgorithm.java b/hutool-crypto/src/main/java/cn/hutool/crypto/digest/HmacAlgorithm.java new file mode 100644 index 000000000..93d9978e1 --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/digest/HmacAlgorithm.java @@ -0,0 +1,27 @@ +package cn.hutool.crypto.digest; + +/** + * HMAC算法类型
+ * see: https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Mac + * + * @author Looly + */ +public enum HmacAlgorithm { + HmacMD5("HmacMD5"), + HmacSHA1("HmacSHA1"), + HmacSHA256("HmacSHA256"), + HmacSHA384("HmacSHA384"), + HmacSHA512("HmacSHA512"), + /** HmacSM3算法实现,需要BouncyCastle库支持 */ + HmacSM3("HmacSM3"); + + private String value; + + private HmacAlgorithm(String value) { + this.value = value; + } + + public String getValue() { + return this.value; + } +} \ No newline at end of file diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/digest/MD5.java b/hutool-crypto/src/main/java/cn/hutool/crypto/digest/MD5.java new file mode 100644 index 000000000..b0d29c1ac --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/digest/MD5.java @@ -0,0 +1,119 @@ +package cn.hutool.crypto.digest; + +import java.io.File; +import java.io.InputStream; +import java.nio.charset.Charset; + +/** + * MD5算法 + * + * @author looly + * @since 4.4.3 + */ +public class MD5 extends Digester { + + /** + * 创建MD5实例 + * + * @return MD5 + * @since 4.6.0 + */ + public static MD5 create() { + return new MD5(); + } + + /** + * 构造 + */ + public MD5() { + super(DigestAlgorithm.MD5); + } + + /** + * 构造 + * + * @param salt 盐值 + */ + public MD5(byte[] salt) { + this(salt, 0, 1); + } + + /** + * 构造 + * + * @param salt 盐值 + * @param digestCount 摘要次数,当此值小于等于1,默认为1。 + */ + public MD5(byte[] salt, int digestCount) { + this(salt, 0, digestCount); + } + + /** + * 构造 + * + * @param salt 盐值 + * @param saltPosition 加盐位置,既将盐值字符串放置在数据的index数,默认0 + * @param digestCount 摘要次数,当此值小于等于1,默认为1。 + */ + public MD5(byte[] salt, int saltPosition, int digestCount) { + this(); + this.salt = salt; + this.saltPosition = saltPosition; + this.digestCount = digestCount; + } + + /** + * 生成16位MD5摘要 + * + * @param data 数据 + * @param charset 编码 + * @return 16位MD5摘要 + * @since 4.6.0 + */ + public String digestHex16(String data, Charset charset) { + return DigestUtil.md5HexTo16(digestHex(data, charset)); + } + + /** + * 生成16位MD5摘要 + * + * @param data 数据 + * @return 16位MD5摘要 + * @since 4.5.1 + */ + public String digestHex16(String data) { + return DigestUtil.md5HexTo16(digestHex(data)); + } + + /** + * 生成16位MD5摘要 + * + * @param data 数据 + * @return 16位MD5摘要 + * @since 4.5.1 + */ + public String digestHex16(InputStream data) { + return DigestUtil.md5HexTo16(digestHex(data)); + } + + /** + * 生成16位MD5摘要 + * + * @param data 数据 + * @return 16位MD5摘要 + */ + public String digestHex16(File data) { + return DigestUtil.md5HexTo16(digestHex(data)); + } + + /** + * 生成16位MD5摘要 + * + * @param data 数据 + * @return 16位MD5摘要 + * @since 4.5.1 + */ + public String digestHex16(byte[] data) { + return DigestUtil.md5HexTo16(digestHex(data)); + } +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/digest/mac/BCHMacEngine.java b/hutool-crypto/src/main/java/cn/hutool/crypto/digest/mac/BCHMacEngine.java new file mode 100644 index 000000000..635880d8a --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/digest/mac/BCHMacEngine.java @@ -0,0 +1,97 @@ +package cn.hutool.crypto.digest.mac; + +import java.io.IOException; +import java.io.InputStream; + +import org.bouncycastle.crypto.CipherParameters; +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.Mac; +import org.bouncycastle.crypto.macs.HMac; +import org.bouncycastle.crypto.params.KeyParameter; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.crypto.CryptoException; + +/** + * BouncyCastle的HMAC算法实现引擎,使用{@link Mac} 实现摘要
+ * 当引入BouncyCastle库时自动使用其作为Provider + * + * @author Looly + * @since 4.5.13 + */ +public class BCHMacEngine implements MacEngine { + + private Mac mac; + + // ------------------------------------------------------------------------------------------- Constructor start + /** + * 构造 + * + * @param digest 摘要算法,为{@link Digest} 的接口实现 + * @param key 密钥 + * @since 4.5.13 + */ + public BCHMacEngine(Digest digest, byte[] key) { + this(digest, new KeyParameter(key)); + } + + /** + * 构造 + * + * @param digest 摘要算法 + * @param params 参数,例如密钥可以用{@link KeyParameter} + * @since 4.5.13 + */ + public BCHMacEngine(Digest digest, CipherParameters params) { + init(digest, params); + } + // ------------------------------------------------------------------------------------------- Constructor end + + /** + * 初始化 + * + * @param digest 摘要算法 + * @param params 参数,例如密钥可以用{@link KeyParameter} + * @return this + */ + public BCHMacEngine init(Digest digest, CipherParameters params) { + mac = new HMac(digest); + mac.init(params); + return this; + } + + @Override + public byte[] digest(InputStream data, int bufferLength) { + if (bufferLength < 1) { + bufferLength = IoUtil.DEFAULT_BUFFER_SIZE; + } + final byte[] buffer = new byte[bufferLength]; + + byte[] result = null; + try { + int read = data.read(buffer, 0, bufferLength); + + while (read > -1) { + mac.update(buffer, 0, read); + read = data.read(buffer, 0, bufferLength); + } + result = new byte[this.mac.getMacSize()]; + mac.doFinal(result, 0); + } catch (IOException e) { + throw new CryptoException(e); + } finally { + mac.reset(); + } + return result; + } + + /** + * 获得 {@link Mac} + * + * @return {@link Mac} + */ + public Mac getMac() { + return mac; + } + +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/digest/mac/DefaultHMacEngine.java b/hutool-crypto/src/main/java/cn/hutool/crypto/digest/mac/DefaultHMacEngine.java new file mode 100644 index 000000000..a1ec7bb43 --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/digest/mac/DefaultHMacEngine.java @@ -0,0 +1,109 @@ +package cn.hutool.crypto.digest.mac; + +import java.io.IOException; +import java.io.InputStream; + +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.crypto.CryptoException; +import cn.hutool.crypto.SecureUtil; + +/** + * 默认的HMAC算法实现引擎,使用{@link Mac} 实现摘要
+ * 当引入BouncyCastle库时自动使用其作为Provider + * + * @author Looly + *@since 4.5.13 + */ +public class DefaultHMacEngine implements MacEngine { + + private Mac mac; + + // ------------------------------------------------------------------------------------------- Constructor start + /** + * 构造 + * @param algorithm 算法 + * @param key 密钥 + * @since 4.5.13 + */ + public DefaultHMacEngine(String algorithm, byte[] key) { + init(algorithm, key); + } + + /** + * 构造 + * @param algorithm 算法 + * @param key 密钥 + * @since 4.5.13 + */ + public DefaultHMacEngine(String algorithm, SecretKey key) { + init(algorithm, key); + } + // ------------------------------------------------------------------------------------------- Constructor end + + /** + * 初始化 + * @param algorithm 算法 + * @param key 密钥 + * @return this + */ + public DefaultHMacEngine init(String algorithm, byte[] key){ + return init(algorithm, (null == key) ? null : new SecretKeySpec(key, algorithm)); + } + + /** + * 初始化 + * @param algorithm 算法 + * @param key 密钥 {@link SecretKey} + * @return this + * @throws CryptoException Cause by IOException + */ + public DefaultHMacEngine init(String algorithm, SecretKey key){ + try { + mac = SecureUtil.createMac(algorithm); + if(null == key){ + key = SecureUtil.generateKey(algorithm); + } + mac.init(key); + } catch (Exception e) { + throw new CryptoException(e); + } + return this; + } + + @Override + public byte[] digest(InputStream data, int bufferLength) { + if (bufferLength < 1) { + bufferLength = IoUtil.DEFAULT_BUFFER_SIZE; + } + byte[] buffer = new byte[bufferLength]; + + byte[] result = null; + try { + int read = data.read(buffer, 0, bufferLength); + + while (read > -1) { + mac.update(buffer, 0, read); + read = data.read(buffer, 0, bufferLength); + } + result = mac.doFinal(); + } catch (IOException e) { + throw new CryptoException(e); + } finally { + mac.reset(); + } + return result; + } + + /** + * 获得 {@link Mac} + * + * @return {@link Mac} + */ + public Mac getMac() { + return mac; + } +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/digest/mac/MacEngine.java b/hutool-crypto/src/main/java/cn/hutool/crypto/digest/mac/MacEngine.java new file mode 100644 index 000000000..85fb8ec95 --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/digest/mac/MacEngine.java @@ -0,0 +1,23 @@ +package cn.hutool.crypto.digest.mac; + +import java.io.InputStream; + +import cn.hutool.core.io.IoUtil; + +/** + * MAC(Message Authentication Code)算法引擎 + * + * @author Looly + * @since 4.5.13 + */ +public interface MacEngine { + + /** + * 生成摘要 + * + * @param data {@link InputStream} 数据流 + * @param bufferLength 缓存长度,不足1使用 {@link IoUtil#DEFAULT_BUFFER_SIZE} 做为默认值 + * @return 摘要bytes + */ + byte[] digest(InputStream data, int bufferLength); +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/digest/mac/MacEngineFactory.java b/hutool-crypto/src/main/java/cn/hutool/crypto/digest/mac/MacEngineFactory.java new file mode 100644 index 000000000..9288b6e72 --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/digest/mac/MacEngineFactory.java @@ -0,0 +1,29 @@ +package cn.hutool.crypto.digest.mac; + +import javax.crypto.SecretKey; + +import cn.hutool.crypto.SmUtil; +import cn.hutool.crypto.digest.HmacAlgorithm; + +/** + * {@link MacEngine} 实现工厂类 + * + * @author Looly + *@since 4.5.13 + */ +public class MacEngineFactory { + + /** + * 根据给定算法和密钥生成对应的{@link MacEngine} + * @param algorithm 算法,见{@link HmacAlgorithm} + * @param key 密钥 + * @return {@link MacEngine} + */ + public static MacEngine createEngine(String algorithm, SecretKey key) { + if(algorithm.equalsIgnoreCase(HmacAlgorithm.HmacSM3.getValue())) { + // HmacSM3算法是BC库实现的 + return SmUtil.createHmacSm3Engine(key.getEncoded()); + } + return new DefaultHMacEngine(algorithm, key); + } +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/digest/mac/package-info.java b/hutool-crypto/src/main/java/cn/hutool/crypto/digest/mac/package-info.java new file mode 100644 index 000000000..d4dcbda40 --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/digest/mac/package-info.java @@ -0,0 +1,10 @@ +/** + * HMAC,全称为“Hash Message Authentication Code”,中文名“散列消息鉴别码”
+ * 主要是利用哈希算法,以一个密钥和一个消息为输入,生成一个消息摘要作为输出。
+ * 一般的,消息鉴别码用于验证传输于两个共 同享有一个密钥的单位之间的消息。
+ * HMAC 可以与任何迭代散列函数捆绑使用。MD5 和 SHA-1 就是这种散列函数。HMAC 还可以使用一个用于计算和确认消息鉴别值的密钥。
+ * + * @author Looly + * @since 4.5.13 + */ +package cn.hutool.crypto.digest.mac; \ No newline at end of file diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/digest/package-info.java b/hutool-crypto/src/main/java/cn/hutool/crypto/digest/package-info.java new file mode 100644 index 000000000..09bd2b442 --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/digest/package-info.java @@ -0,0 +1,7 @@ +/** + * 摘要加密算法实现,入口为DigestUtil + * + * @author looly + * + */ +package cn.hutool.crypto.digest; \ No newline at end of file diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/package-info.java b/hutool-crypto/src/main/java/cn/hutool/crypto/package-info.java new file mode 100644 index 000000000..746bb5b2f --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/package-info.java @@ -0,0 +1,13 @@ +/** + * 加密解密模块,实现了对JDK中加密解密算法的封装,入口为SecureUtil,实现了: + * + *
+ * 1. 对称加密(symmetric),例如:AES、DES等
+ * 2. 非对称加密(asymmetric),例如:RSA、DSA等
+ * 3. 摘要加密(digest),例如:MD5、SHA-1、SHA-256、HMAC等
+ * 
+ * + * @author looly + * + */ +package cn.hutool.crypto; \ No newline at end of file diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/AES.java b/hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/AES.java new file mode 100644 index 000000000..4472fc06f --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/AES.java @@ -0,0 +1,177 @@ +package cn.hutool.crypto.symmetric; + +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.Mode; +import cn.hutool.crypto.Padding; +import cn.hutool.crypto.SecureUtil; + +/** + * AES加密算法实现
+ * 高级加密标准(英语:Advanced Encryption Standard,缩写:AES),在密码学中又称Rijndael加密法
+ * 对于Java中AES的默认模式是:AES/ECB/PKCS5Padding,如果使用CryptoJS,请调整为:padding: CryptoJS.pad.Pkcs7 + * + * @author Looly + * @since 3.0.8 + */ +public class AES extends SymmetricCrypto { + + //------------------------------------------------------------------------- Constrctor start + /** + * 构造,默认AES/ECB/PKCS5Padding,使用随机密钥 + */ + public AES() { + super(SymmetricAlgorithm.AES); + } + + /** + * 构造,使用默认的AES/ECB/PKCS5Padding + * + * @param key 密钥 + */ + public AES(byte[] key) { + super(SymmetricAlgorithm.AES, key); + } + + /** + * 构造,使用随机密钥 + * + * @param mode 模式{@link Mode} + * @param padding {@link Padding}补码方式 + */ + public AES(Mode mode, Padding padding) { + this(mode.name(), padding.name()); + } + + /** + * 构造 + * + * @param mode 模式{@link Mode} + * @param padding {@link Padding}补码方式 + * @param key 密钥,支持三种密钥长度:128、192、256位 + */ + public AES(Mode mode, Padding padding, byte[] key) { + this(mode, padding, key, null); + } + + /** + * 构造 + * + * @param mode 模式{@link Mode} + * @param padding {@link Padding}补码方式 + * @param key 密钥,支持三种密钥长度:128、192、256位 + * @param iv 偏移向量,加盐 + * @since 3.3.0 + */ + public AES(Mode mode, Padding padding, byte[] key, byte[] iv) { + this(mode.name(), padding.name(), key, iv); + } + + /** + * 构造 + * + * @param mode 模式{@link Mode} + * @param padding {@link Padding}补码方式 + * @param key 密钥,支持三种密钥长度:128、192、256位 + * @since 3.3.0 + */ + public AES(Mode mode, Padding padding, SecretKey key) { + this(mode, padding, key, null); + } + + /** + * 构造 + * + * @param mode 模式{@link Mode} + * @param padding {@link Padding}补码方式 + * @param key 密钥,支持三种密钥长度:128、192、256位 + * @param iv 偏移向量,加盐 + * @since 3.3.0 + */ + public AES(Mode mode, Padding padding, SecretKey key, IvParameterSpec iv) { + this(mode.name(), padding.name(), key, iv); + } + + /** + * 构造 + * + * @param mode 模式 + * @param padding 补码方式 + */ + public AES(String mode, String padding) { + this(mode, padding, (byte[]) null); + } + + /** + * 构造 + * + * @param mode 模式 + * @param padding 补码方式 + * @param key 密钥,支持三种密钥长度:128、192、256位 + */ + public AES(String mode, String padding, byte[] key) { + this(mode, padding, key, null); + } + + /** + * 构造 + * + * @param mode 模式 + * @param padding 补码方式 + * @param key 密钥,支持三种密钥长度:128、192、256位 + * @param iv 加盐 + */ + public AES(String mode, String padding, byte[] key, byte[] iv) { + this(mode, padding, SecureUtil.generateKey(SymmetricAlgorithm.AES.getValue(), key), null == iv ? null : new IvParameterSpec(iv)); + } + + /** + * 构造 + * + * @param mode 模式 + * @param padding 补码方式 + * @param key 密钥,支持三种密钥长度:128、192、256位 + */ + public AES(String mode, String padding, SecretKey key) { + this(mode, padding, key, null); + } + + /** + * 构造 + * + * @param mode 模式 + * @param padding 补码方式 + * @param key 密钥,支持三种密钥长度:128、192、256位 + * @param iv 加盐 + */ + public AES(String mode, String padding, SecretKey key, IvParameterSpec iv) { + super(StrUtil.format("AES/{}/{}", mode, padding), key, iv); + } + //------------------------------------------------------------------------- Constrctor end + + /** + * 设置偏移向量 + * + * @param iv {@link IvParameterSpec}偏移向量 + * @return 自身 + */ + public AES setIv(IvParameterSpec iv) { + super.setParams(iv); + return this; + } + + /** + * 设置偏移向量 + * + * @param iv 偏移向量,加盐 + * @return 自身 + * @since 3.3.0 + */ + public AES setIv(byte[] iv) { + setIv(new IvParameterSpec(iv)); + return this; + } + +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/DES.java b/hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/DES.java new file mode 100644 index 000000000..55eeae534 --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/DES.java @@ -0,0 +1,177 @@ +package cn.hutool.crypto.symmetric; + +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.Mode; +import cn.hutool.crypto.Padding; +import cn.hutool.crypto.SecureUtil; + +/** + * DES加密算法实现
+ * DES全称为Data Encryption Standard,即数据加密标准,是一种使用密钥加密的块算法
+ * Java中默认实现为:DES/CBC/PKCS5Padding + * + * @author Looly + * @since 3.0.8 + */ +public class DES extends SymmetricCrypto { + + // ------------------------------------------------------------------------- Constrctor start + /** + * 构造,默认DES/CBC/PKCS5Padding,使用随机密钥 + */ + public DES() { + super(SymmetricAlgorithm.DES); + } + + /** + * 构造,使用默认的DES/CBC/PKCS5Padding + * + * @param key 密钥 + */ + public DES(byte[] key) { + super(SymmetricAlgorithm.DES, key); + } + + /** + * 构造,使用随机密钥 + * + * @param mode 模式{@link Mode} + * @param padding {@link Padding}补码方式 + */ + public DES(Mode mode, Padding padding) { + this(mode.name(), padding.name()); + } + + /** + * 构造 + * + * @param mode 模式{@link Mode} + * @param padding {@link Padding}补码方式 + * @param key 密钥,长度:8的倍数 + */ + public DES(Mode mode, Padding padding, byte[] key) { + this(mode, padding, key, null); + } + + /** + * 构造 + * + * @param mode 模式{@link Mode} + * @param padding {@link Padding}补码方式 + * @param key 密钥,长度:8的倍数 + * @param iv 偏移向量,加盐 + * @since 3.3.0 + */ + public DES(Mode mode, Padding padding, byte[] key, byte[] iv) { + this(mode.name(), padding.name(), key, iv); + } + + /** + * 构造 + * + * @param mode 模式{@link Mode} + * @param padding {@link Padding}补码方式 + * @param key 密钥,长度:8的倍数 + * @since 3.3.0 + */ + public DES(Mode mode, Padding padding, SecretKey key) { + this(mode, padding, key, null); + } + + /** + * 构造 + * + * @param mode 模式{@link Mode} + * @param padding {@link Padding}补码方式 + * @param key 密钥,长度:8的倍数 + * @param iv 偏移向量,加盐 + * @since 3.3.0 + */ + public DES(Mode mode, Padding padding, SecretKey key, IvParameterSpec iv) { + this(mode.name(), padding.name(), key, iv); + } + + /** + * 构造 + * + * @param mode 模式 + * @param padding 补码方式 + */ + public DES(String mode, String padding) { + this(mode, padding, (byte[]) null); + } + + /** + * 构造 + * + * @param mode 模式 + * @param padding 补码方式 + * @param key 密钥,长度:8的倍数 + */ + public DES(String mode, String padding, byte[] key) { + this(mode, padding, SecureUtil.generateKey("DES", key), null); + } + + /** + * 构造 + * + * @param mode 模式 + * @param padding 补码方式 + * @param key 密钥,长度:8的倍数 + * @param iv 加盐 + */ + public DES(String mode, String padding, byte[] key, byte[] iv) { + this(mode, padding, SecureUtil.generateKey("DES", key), null == iv ? null : new IvParameterSpec(iv)); + } + + /** + * 构造 + * + * @param mode 模式 + * @param padding 补码方式 + * @param key 密钥,长度:8的倍数 + */ + public DES(String mode, String padding, SecretKey key) { + this(mode, padding, key, null); + } + + /** + * 构造 + * + * @param mode 模式 + * @param padding 补码方式 + * @param key 密钥,长度:8的倍数 + * @param iv 加盐 + */ + public DES(String mode, String padding, SecretKey key, IvParameterSpec iv) { + super(StrUtil.format("DES/{}/{}", mode, padding), key, iv); + } + // ------------------------------------------------------------------------- Constrctor end + + /** + * 设置偏移向量 + * + * @param iv {@link IvParameterSpec}偏移向量 + * @return 自身 + */ + public DES setIv(IvParameterSpec iv) { + super.setParams(iv); + return this; + } + + /** + * 设置偏移向量 + * + * @param iv 偏移向量,加盐 + * @return 自身 + * @since 3.3.0 + */ + public DES setIv(byte[] iv) { + setIv(new IvParameterSpec(iv)); + return this; + } + +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/DESede.java b/hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/DESede.java new file mode 100644 index 000000000..46af9f468 --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/DESede.java @@ -0,0 +1,178 @@ +package cn.hutool.crypto.symmetric; + +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.Mode; +import cn.hutool.crypto.Padding; +import cn.hutool.crypto.SecureUtil; + +/** + * DESede是由DES对称加密算法改进后的一种对称加密算法,又名3DES、TripleDES。
+ * 使用 168 位的密钥对资料进行三次加密的一种机制;它通常(但非始终)提供极其强大的安全性。
+ * 如果三个 56 位的子元素都相同,则三重 DES 向后兼容 DES。
+ * Java中默认实现为:DESede/ECB/PKCS5Padding + * + * @author Looly + * @since 3.3.0 + */ +public class DESede extends SymmetricCrypto { + + // ------------------------------------------------------------------------- Constructor start + /** + * 构造,默认DESede/ECB/PKCS5Padding,使用随机密钥 + */ + public DESede() { + super(SymmetricAlgorithm.DESede); + } + + /** + * 构造,使用默认的DESede/ECB/PKCS5Padding + * + * @param key 密钥 + */ + public DESede(byte[] key) { + super(SymmetricAlgorithm.DESede, key); + } + + /** + * 构造,使用随机密钥 + * + * @param mode 模式{@link Mode} + * @param padding {@link Padding}补码方式 + */ + public DESede(Mode mode, Padding padding) { + this(mode.name(), padding.name()); + } + + /** + * 构造 + * + * @param mode 模式{@link Mode} + * @param padding {@link Padding}补码方式 + * @param key 密钥,长度24位 + */ + public DESede(Mode mode, Padding padding, byte[] key) { + this(mode, padding, key, null); + } + + /** + * 构造 + * + * @param mode 模式{@link Mode} + * @param padding {@link Padding}补码方式 + * @param key 密钥,长度24位 + * @param iv 偏移向量,加盐 + * @since 3.3.0 + */ + public DESede(Mode mode, Padding padding, byte[] key, byte[] iv) { + this(mode.name(), padding.name(), key, iv); + } + + /** + * 构造 + * + * @param mode 模式{@link Mode} + * @param padding {@link Padding}补码方式 + * @param key 密钥,长度24位 + * @since 3.3.0 + */ + public DESede(Mode mode, Padding padding, SecretKey key) { + this(mode, padding, key, null); + } + + /** + * 构造 + * + * @param mode 模式{@link Mode} + * @param padding {@link Padding}补码方式 + * @param key 密钥,长度24位 + * @param iv 偏移向量,加盐 + * @since 3.3.0 + */ + public DESede(Mode mode, Padding padding, SecretKey key, IvParameterSpec iv) { + this(mode.name(), padding.name(), key, iv); + } + + /** + * 构造 + * + * @param mode 模式 + * @param padding 补码方式 + */ + public DESede(String mode, String padding) { + this(mode, padding, (byte[]) null); + } + + /** + * 构造 + * + * @param mode 模式 + * @param padding 补码方式 + * @param key 密钥,长度24位 + */ + public DESede(String mode, String padding, byte[] key) { + this(mode, padding, key, null); + } + + /** + * 构造 + * + * @param mode 模式 + * @param padding 补码方式 + * @param key 密钥,长度24位 + * @param iv 加盐 + */ + public DESede(String mode, String padding, byte[] key, byte[] iv) { + this(mode, padding, SecureUtil.generateKey(SymmetricAlgorithm.DESede.getValue(), key), null == iv ? null : new IvParameterSpec(iv)); + } + + /** + * 构造 + * + * @param mode 模式 + * @param padding 补码方式 + * @param key 密钥 + */ + public DESede(String mode, String padding, SecretKey key) { + this(mode, padding, key, null); + } + + /** + * 构造 + * + * @param mode 模式 + * @param padding 补码方式 + * @param key 密钥 + * @param iv 加盐 + */ + public DESede(String mode, String padding, SecretKey key, IvParameterSpec iv) { + super(StrUtil.format("{}/{}/{}", SymmetricAlgorithm.DESede.getValue(), mode, padding), key, iv); + } + // ------------------------------------------------------------------------- Constructor end + + /** + * 设置偏移向量 + * + * @param iv {@link IvParameterSpec}偏移向量 + * @return 自身 + */ + public DESede setIv(IvParameterSpec iv) { + super.setParams(iv); + return this; + } + + /** + * 设置偏移向量 + * + * @param iv 偏移向量,加盐 + * @return 自身 + * @since 3.3.0 + */ + public DESede setIv(byte[] iv) { + setIv(new IvParameterSpec(iv)); + return this; + } + +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/RC4.java b/hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/RC4.java new file mode 100644 index 000000000..8bd8a7360 --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/RC4.java @@ -0,0 +1,220 @@ +package cn.hutool.crypto.symmetric; + +import java.nio.charset.Charset; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; +import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.HexUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.CryptoException; + +/** + * RC4加密解密算法实现
+ * 来自:https://github.com/xSAVIKx/RC4-cipher/blob/master/src/main/java/com/github/xsavikx/rc4/RC4.java + * + * @author Iurii Sergiichuk,Looly + */ +public class RC4 { + + private static final int SBOX_LENGTH = 256; + /** 密钥最小长度 */ + private static final int KEY_MIN_LENGTH = 5; + + /** Key array */ + private byte[] key; + /** Sbox */ + private int[] sbox; + + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + + /** + * 构造 + * + * @param key 密钥 + * @throws CryptoException + */ + public RC4(String key) throws CryptoException { + setKey(key); + } + + /** + * 加密 + * + * @param message 消息 + * @param charset 编码 + * @return 密文 + * @throws CryptoException key长度小于5或者大于255抛出此异常 + */ + public byte[] encrypt(String message, Charset charset) throws CryptoException { + return crypt(StrUtil.bytes(message, charset)); + } + + /** + * 加密,使用默认编码:UTF-8 + * + * @param message 消息 + * @return 密文 + * @throws CryptoException key长度小于5或者大于255抛出此异常 + */ + public byte[] encrypt(String message) throws CryptoException { + return encrypt(message, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 加密 + * + * @param data 数据 + * @return 加密后的Hex + * @since 4.5.12 + */ + public String encryptHex(byte[] data) { + return HexUtil.encodeHexStr(crypt(data)); + } + + /** + * 加密 + * + * @param data 数据 + * @return 加密后的Base64 + * @since 4.5.12 + */ + public String encryptBase64(byte[] data) { + return Base64.encode(crypt(data)); + } + + /** + * 加密 + * + * @param data 被加密的字符串 + * @param charset 编码 + * @return 加密后的Hex + * @since 4.5.12 + */ + public String encryptHex(String data, Charset charset) { + return HexUtil.encodeHexStr(encrypt(data, charset)); + } + + /** + * 加密 + * + * @param data 被加密的字符串 + * @param charset 编码 + * @return 加密后的Base64 + * @since 4.5.12 + */ + public String encryptBase64(String data, Charset charset) { + return Base64.encode(encrypt(data, charset)); + } + + /** + * 解密 + * + * @param message 消息 + * @param charset 编码 + * @return 明文 + * @throws CryptoException key长度小于5或者大于255抛出此异常 + */ + public String decrypt(byte[] message, Charset charset) throws CryptoException { + return StrUtil.str(crypt(message), charset); + } + + /** + * 解密,使用默认编码UTF-8 + * + * @param message 消息 + * @return 明文 + * @throws CryptoException key长度小于5或者大于255抛出此异常 + */ + public String decrypt(byte[] message) throws CryptoException { + return decrypt(message, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 加密或解密指定值,调用此方法前需初始化密钥 + * + * @param msg 要加密或解密的消息 + * @return 加密或解密后的值 + */ + public byte[] crypt(final byte[] msg) { + final ReadLock readLock = this.lock.readLock(); + readLock.lock(); + byte[] code; + try { + final int[] sbox = this.sbox.clone(); + code = new byte[msg.length]; + int i = 0; + int j = 0; + for (int n = 0; n < msg.length; n++) { + i = (i + 1) % SBOX_LENGTH; + j = (j + sbox[i]) % SBOX_LENGTH; + swap(i, j, sbox); + int rand = sbox[(sbox[i] + sbox[j]) % SBOX_LENGTH]; + code[n] = (byte) (rand ^ msg[n]); + } + } finally { + readLock.unlock(); + } + return code; + } + + /** + * 设置密钥 + * + * @param key 密钥 + * @throws CryptoException key长度小于5或者大于255抛出此异常 + */ + public void setKey(String key) throws CryptoException { + final int length = key.length(); + if (length < KEY_MIN_LENGTH || length >= SBOX_LENGTH) { + throw new CryptoException("Key length has to be between {} and {}", KEY_MIN_LENGTH, (SBOX_LENGTH - 1)); + } + + final WriteLock writeLock = this.lock.writeLock(); + writeLock.lock(); + try { + this.key = StrUtil.utf8Bytes(key); + this.sbox = initSBox(this.key); + } finally { + writeLock.unlock(); + } + } + + //----------------------------------------------------------------------------------------------------------------------- Private method start + /** + * 初始化Sbox + * + * @param key 密钥 + * @return sbox + */ + private int[] initSBox(byte[] key) { + int[] sbox = new int[SBOX_LENGTH]; + int j = 0; + + for (int i = 0; i < SBOX_LENGTH; i++) { + sbox[i] = i; + } + + for (int i = 0; i < SBOX_LENGTH; i++) { + j = (j + sbox[i] + (key[i % key.length]) & 0xFF) % SBOX_LENGTH; + swap(i, j, sbox); + } + return sbox; + } + + /** + * 交换指定两个位置的值 + * + * @param i 位置1 + * @param j 位置2 + * @param sbox 数组 + */ + private void swap(int i, int j, int[] sbox) { + int temp = sbox[i]; + sbox[i] = sbox[j]; + sbox[j] = temp; + } + //----------------------------------------------------------------------------------------------------------------------- Private method end +} \ No newline at end of file diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/SymmetricAlgorithm.java b/hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/SymmetricAlgorithm.java new file mode 100644 index 000000000..30ce04944 --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/SymmetricAlgorithm.java @@ -0,0 +1,42 @@ +package cn.hutool.crypto.symmetric; + +/** + * 对称算法类型
+ * see: https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyGenerator + * + * @author Looly + * + */ +public enum SymmetricAlgorithm { + /** 默认的AES加密方式:AES/CBC/PKCS5Padding */ + AES("AES"), + ARCFOUR("ARCFOUR"), + Blowfish("Blowfish"), + /** 默认的DES加密方式:DES/ECB/PKCS5Padding */ + DES("DES"), + /** 3DES算法,默认实现为:DESede/CBC/PKCS5Padding */ + DESede("DESede"), + RC2("RC2"), + + PBEWithMD5AndDES("PBEWithMD5AndDES"), + PBEWithSHA1AndDESede("PBEWithSHA1AndDESede"), + PBEWithSHA1AndRC2_40("PBEWithSHA1AndRC2_40"); + + private String value; + + /** + * 构造 + * @param value 算法的字符串表示,区分大小写 + */ + private SymmetricAlgorithm(String value) { + this.value = value; + } + + /** + * 获得算法的字符串表示形式 + * @return 算法字符串 + */ + public String getValue() { + return this.value; + } +} \ No newline at end of file diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/SymmetricCrypto.java b/hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/SymmetricCrypto.java new file mode 100644 index 000000000..d493421c3 --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/SymmetricCrypto.java @@ -0,0 +1,447 @@ +package cn.hutool.crypto.symmetric; + +import java.io.InputStream; +import java.nio.charset.Charset; +import java.security.spec.AlgorithmParameterSpec; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.PBEParameterSpec; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.HexUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.CryptoException; +import cn.hutool.crypto.KeyUtil; +import cn.hutool.crypto.SecureUtil; + +/** + * 对称加密算法
+ * 在对称加密算法中,数据发信方将明文(原始数据)和加密密钥一起经过特殊加密算法处理后,使其变成复杂的加密密文发送出去。
+ * 收信方收到密文后,若想解读原文,则需要使用加密用过的密钥及相同算法的逆算法对密文进行解密,才能使其恢复成可读明文。
+ * 在对称加密算法中,使用的密钥只有一个,发收信双方都使用这个密钥对数据进行加密和解密,这就要求解密方事先必须知道加密密钥。
+ * + * @author Looly + * + */ +public class SymmetricCrypto { + + /** SecretKey 负责保存对称密钥 */ + private SecretKey secretKey; + /** Cipher负责完成加密或解密工作 */ + private Cipher cipher; + /** 加密解密参数 */ + private AlgorithmParameterSpec params; + private Lock lock = new ReentrantLock(); + + // ------------------------------------------------------------------ Constructor start + /** + * 构造,使用随机密钥 + * + * @param algorithm {@link SymmetricAlgorithm} + */ + public SymmetricCrypto(SymmetricAlgorithm algorithm) { + this(algorithm, (byte[]) null); + } + + /** + * 构造,使用随机密钥 + * + * @param algorithm 算法,可以是"algorithm/mode/padding"或者"algorithm" + */ + public SymmetricCrypto(String algorithm) { + this(algorithm, (byte[]) null); + } + + /** + * 构造 + * + * @param algorithm 算法 {@link SymmetricAlgorithm} + * @param key 自定义KEY + */ + public SymmetricCrypto(SymmetricAlgorithm algorithm, byte[] key) { + this(algorithm.getValue(), key); + } + + /** + * 构造 + * + * @param algorithm 算法 {@link SymmetricAlgorithm} + * @param key 自定义KEY + * @since 3.1.2 + */ + public SymmetricCrypto(SymmetricAlgorithm algorithm, SecretKey key) { + this(algorithm.getValue(), key); + } + + /** + * 构造 + * + * @param algorithm 算法 + * @param key 密钥 + */ + public SymmetricCrypto(String algorithm, byte[] key) { + this(algorithm, KeyUtil.generateKey(algorithm, key)); + } + + /** + * 构造 + * + * @param algorithm 算法 + * @param key 密钥 + * @since 3.1.2 + */ + public SymmetricCrypto(String algorithm, SecretKey key) { + this(algorithm, key, null); + } + + /** + * 构造 + * + * @param algorithm 算法 + * @param key 密钥 + * @param paramsSpec 算法参数,例如加盐等 + * @since 3.3.0 + */ + public SymmetricCrypto(String algorithm, SecretKey key, AlgorithmParameterSpec paramsSpec) { + init(algorithm, key); + if (null != paramsSpec) { + setParams(paramsSpec); + } + } + + // ------------------------------------------------------------------ Constructor end + /** + * 初始化 + * + * @param algorithm 算法 + * @param key 密钥,如果为null自动生成一个key + * @return {@link SymmetricCrypto} + */ + public SymmetricCrypto init(String algorithm, SecretKey key) { + this.secretKey = key; + if (algorithm.startsWith("PBE")) { + // 对于PBE算法使用随机数加盐 + this.params = new PBEParameterSpec(RandomUtil.randomBytes(8), 100); + } + this.cipher = SecureUtil.createCipher(algorithm); + return this; + } + + /** + * 设置 {@link AlgorithmParameterSpec},通常用于加盐或偏移向量 + * + * @param params {@link AlgorithmParameterSpec} + * @return 自身 + */ + public SymmetricCrypto setParams(AlgorithmParameterSpec params) { + this.params = params; + return this; + } + + // --------------------------------------------------------------------------------- Encrypt + /** + * 加密 + * + * @param data 被加密的bytes + * @return 加密后的bytes + */ + public byte[] encrypt(byte[] data) { + lock.lock(); + try { + if (null == this.params) { + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + } else { + cipher.init(Cipher.ENCRYPT_MODE, secretKey, params); + } + return cipher.doFinal(data); + } catch (Exception e) { + throw new CryptoException(e); + } finally { + lock.unlock(); + } + } + + /** + * 加密 + * + * @param data 数据 + * @return 加密后的Hex + */ + public String encryptHex(byte[] data) { + return HexUtil.encodeHexStr(encrypt(data)); + } + + /** + * 加密 + * + * @param data 数据 + * @return 加密后的Base64 + * @since 4.0.1 + */ + public String encryptBase64(byte[] data) { + return Base64.encode(encrypt(data)); + } + + /** + * 加密 + * + * @param data 被加密的字符串 + * @param charset 编码 + * @return 加密后的bytes + */ + public byte[] encrypt(String data, String charset) { + return encrypt(StrUtil.bytes(data, charset)); + } + + /** + * 加密 + * + * @param data 被加密的字符串 + * @param charset 编码 + * @return 加密后的bytes + */ + public byte[] encrypt(String data, Charset charset) { + return encrypt(StrUtil.bytes(data, charset)); + } + + /** + * 加密 + * + * @param data 被加密的字符串 + * @param charset 编码 + * @return 加密后的Hex + * @since 4.5.12 + */ + public String encryptHex(String data, String charset) { + return HexUtil.encodeHexStr(encrypt(data, charset)); + } + + /** + * 加密 + * + * @param data 被加密的字符串 + * @param charset 编码 + * @return 加密后的Hex + * @since 4.5.12 + */ + public String encryptHex(String data, Charset charset) { + return HexUtil.encodeHexStr(encrypt(data, charset)); + } + + /** + * 加密 + * + * @param data 被加密的字符串 + * @param charset 编码 + * @return 加密后的Base64 + */ + public String encryptBase64(String data, String charset) { + return Base64.encode(encrypt(data, charset)); + } + + /** + * 加密 + * + * @param data 被加密的字符串 + * @param charset 编码 + * @return 加密后的Base64 + * @since 4.5.12 + */ + public String encryptBase64(String data, Charset charset) { + return Base64.encode(encrypt(data, charset)); + } + + /** + * 加密,使用UTF-8编码 + * + * @param data 被加密的字符串 + * @return 加密后的bytes + */ + public byte[] encrypt(String data) { + return encrypt(StrUtil.bytes(data, CharsetUtil.CHARSET_UTF_8)); + } + + /** + * 加密,使用UTF-8编码 + * + * @param data 被加密的字符串 + * @return 加密后的Hex + */ + public String encryptHex(String data) { + return HexUtil.encodeHexStr(encrypt(data)); + } + + /** + * 加密,使用UTF-8编码 + * + * @param data 被加密的字符串 + * @return 加密后的Base64 + */ + public String encryptBase64(String data) { + return Base64.encode(encrypt(data)); + } + + /** + * 加密 + * + * @param data 被加密的字符串 + * @return 加密后的bytes + * @throws IORuntimeException IO异常 + */ + public byte[] encrypt(InputStream data) throws IORuntimeException { + return encrypt(IoUtil.readBytes(data)); + } + + /** + * 加密 + * + * @param data 被加密的字符串 + * @return 加密后的Hex + */ + public String encryptHex(InputStream data) { + return HexUtil.encodeHexStr(encrypt(data)); + } + + /** + * 加密 + * + * @param data 被加密的字符串 + * @return 加密后的Base64 + */ + public String encryptBase64(InputStream data) { + return Base64.encode(encrypt(data)); + } + + // --------------------------------------------------------------------------------- Decrypt + /** + * 解密 + * + * @param bytes 被解密的bytes + * @return 解密后的bytes + */ + public byte[] decrypt(byte[] bytes) { + lock.lock(); + try { + if (null == this.params) { + cipher.init(Cipher.DECRYPT_MODE, secretKey); + } else { + cipher.init(Cipher.DECRYPT_MODE, secretKey, params); + } + return cipher.doFinal(bytes); + } catch (Exception e) { + throw new CryptoException(e); + } finally { + lock.unlock(); + } + } + + /** + * 解密为字符串 + * + * @param bytes 被解密的bytes + * @param charset 解密后的charset + * @return 解密后的String + */ + public String decryptStr(byte[] bytes, Charset charset) { + return StrUtil.str(decrypt(bytes), charset); + } + + /** + * 解密为字符串,默认UTF-8编码 + * + * @param bytes 被解密的bytes + * @return 解密后的String + */ + public String decryptStr(byte[] bytes) { + return decryptStr(bytes, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 解密Hex(16进制)或Base64表示的字符串 + * + * @param data 被解密的String,必须为16进制字符串或Base64表示形式 + * @return 解密后的bytes + */ + public byte[] decrypt(String data) { + return decrypt(SecureUtil.decode(data)); + } + + /** + * 解密Hex(16进制)或Base64表示的字符串 + * + * @param data 被解密的String + * @param charset 解密后的charset + * @return 解密后的String + */ + public String decryptStr(String data, Charset charset) { + return StrUtil.str(decrypt(data), charset); + } + + /** + * 解密Hex表示的字符串,默认UTF-8编码 + * + * @param data 被解密的String + * @return 解密后的String + */ + public String decryptStr(String data) { + return decryptStr(data, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 解密,不会关闭流 + * + * @param data 被解密的bytes + * @return 解密后的bytes + * @throws IORuntimeException IO异常 + */ + public byte[] decrypt(InputStream data) throws IORuntimeException { + return decrypt(IoUtil.readBytes(data)); + } + + /** + * 解密,不会关闭流 + * + * @param data 被解密的InputStream + * @param charset 解密后的charset + * @return 解密后的String + */ + public String decryptStr(InputStream data, Charset charset) { + return StrUtil.str(decrypt(data), charset); + } + + /** + * 解密 + * + * @param data 被解密的InputStream + * @return 解密后的String + */ + public String decryptStr(InputStream data) { + return decryptStr(data, CharsetUtil.CHARSET_UTF_8); + } + + // --------------------------------------------------------------------------------- Getters + /** + * 获得对称密钥 + * + * @return 获得对称密钥 + */ + public SecretKey getSecretKey() { + return secretKey; + } + + /** + * 获得加密或解密器 + * + * @return 加密或解密 + */ + public Cipher getClipher() { + return cipher; + } +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/Vigenere.java b/hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/Vigenere.java new file mode 100644 index 000000000..a4b3bf72e --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/Vigenere.java @@ -0,0 +1,65 @@ +package cn.hutool.crypto.symmetric; + +/** + * 维吉尼亚密码实现。
+ * 人们在恺撒移位密码的基础上扩展出多表密码,称为维吉尼亚密码。
+ * 算法实现来自:https://github.com/zhaorenjie110/SymmetricEncryptionAndDecryption + * + * @author looly,zhaorenjie110 + * @since 4.4.1 + */ +public class Vigenere { + + /** + * 加密 + * + * @param data 数据 + * @param cipherKey 密钥 + * @return 密文 + */ + public static String encrypt(CharSequence data, CharSequence cipherKey) { + final int dataLen = data.length(); + final int cipherKeyLen = cipherKey.length(); + + final char[] cipherArray = new char[dataLen]; + for (int i = 0; i < dataLen / cipherKeyLen + 1; i++) { + for (int t = 0; t < cipherKeyLen; t++) { + if (t + i * cipherKeyLen < dataLen) { + final char dataChar = data.charAt(t + i * cipherKeyLen); + final char cipherKeyChar = cipherKey.charAt(t); + cipherArray[t + i * cipherKeyLen] = (char) ((dataChar + cipherKeyChar - 64) % 95 + 32); + } + } + } + + return String.valueOf(cipherArray); + } + + /** + * 解密 + * + * @param data 密文 + * @param cipherKey 密钥 + * @return 明文 + */ + public static String decrypt(CharSequence data, CharSequence cipherKey) { + final int dataLen = data.length(); + final int cipherKeyLen = cipherKey.length(); + + final char[] clearArray = new char[dataLen]; + for (int i = 0; i < dataLen; i++) { + for (int t = 0; t < cipherKeyLen; t++) { + if (t + i * cipherKeyLen < dataLen) { + final char dataChar = data.charAt(t + i * cipherKeyLen); + final char cipherKeyChar = cipherKey.charAt(t); + if (dataChar - cipherKeyChar >= 0) { + clearArray[t + i * cipherKeyLen] = (char) ((dataChar - cipherKeyChar) % 95 + 32); + } else { + clearArray[t + i * cipherKeyLen] = (char) ((dataChar - cipherKeyChar + 95) % 95 + 32); + } + } + } + } + return String.valueOf(clearArray); + } +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/package-info.java b/hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/package-info.java new file mode 100644 index 000000000..cdead5628 --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/symmetric/package-info.java @@ -0,0 +1,7 @@ +/** + * 对称加密算法实现,包括AES、DES、DESede等 + * + * @author looly + * + */ +package cn.hutool.crypto.symmetric; \ No newline at end of file diff --git a/hutool-crypto/src/test/java/cn/hutool/crypto/test/BCUtilTest.java b/hutool-crypto/src/test/java/cn/hutool/crypto/test/BCUtilTest.java new file mode 100644 index 000000000..f5077bdc8 --- /dev/null +++ b/hutool-crypto/src/test/java/cn/hutool/crypto/test/BCUtilTest.java @@ -0,0 +1,40 @@ +package cn.hutool.crypto.test; + +import java.security.PrivateKey; +import java.security.PublicKey; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.crypto.BCUtil; +import cn.hutool.crypto.asymmetric.KeyType; +import cn.hutool.crypto.asymmetric.RSA; + +public class BCUtilTest { + + @Test + public void readPrivateKeyTest() { + PrivateKey privateKey = BCUtil.readPrivateKey(ResourceUtil.getStream("test_private_key.pem")); + Assert.assertNotNull(privateKey); + } + + @Test + public void readPublicKeyTest() { + PublicKey publicKey = BCUtil.readPublicKey(ResourceUtil.getStream("test_public_key.csr")); + Assert.assertNotNull(publicKey); + } + + @Test + public void validateKey() { + PrivateKey privateKey = BCUtil.readPrivateKey(ResourceUtil.getStream("test_private_key.pem")); + PublicKey publicKey = BCUtil.readPublicKey(ResourceUtil.getStream("test_public_key.csr")); + + RSA rsa = new RSA(privateKey, publicKey); + String str = "你好,Hutool";//测试字符串 + + String encryptStr = rsa.encryptBase64(str, KeyType.PublicKey); + String decryptStr = rsa.decryptStr(encryptStr, KeyType.PrivateKey); + Assert.assertEquals(str, decryptStr); + } +} diff --git a/hutool-crypto/src/test/java/cn/hutool/crypto/test/HmacTest.java b/hutool-crypto/src/test/java/cn/hutool/crypto/test/HmacTest.java new file mode 100644 index 000000000..1fd247926 --- /dev/null +++ b/hutool-crypto/src/test/java/cn/hutool/crypto/test/HmacTest.java @@ -0,0 +1,58 @@ +package cn.hutool.crypto.test; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.crypto.SecureUtil; +import cn.hutool.crypto.digest.HMac; +import cn.hutool.crypto.digest.HmacAlgorithm; + +/** + * Hmac单元测试 + * @author Looly + * + */ +public class HmacTest { + + @Test + public void hmacTest(){ + String testStr = "test中文"; + + byte[] key = "password".getBytes(); + HMac mac = new HMac(HmacAlgorithm.HmacMD5, key); + + String macHex1 = mac.digestHex(testStr); + Assert.assertEquals("b977f4b13f93f549e06140971bded384", macHex1); + + String macHex2 = mac.digestHex(IoUtil.toStream(testStr, CharsetUtil.CHARSET_UTF_8)); + Assert.assertEquals("b977f4b13f93f549e06140971bded384", macHex2); + } + + @Test + public void hmacMd5Test(){ + String testStr = "test中文"; + + HMac mac = SecureUtil.hmacMd5("password"); + + String macHex1 = mac.digestHex(testStr); + Assert.assertEquals("b977f4b13f93f549e06140971bded384", macHex1); + + String macHex2 = mac.digestHex(IoUtil.toStream(testStr, CharsetUtil.CHARSET_UTF_8)); + Assert.assertEquals("b977f4b13f93f549e06140971bded384", macHex2); + } + + @Test + public void hmacSha1Test(){ + String testStr = "test中文"; + + HMac mac = SecureUtil.hmacSha1("password"); + + String macHex1 = mac.digestHex(testStr); + Assert.assertEquals("1dd68d2f119d5640f0d416e99d3f42408b88d511", macHex1); + + String macHex2 = mac.digestHex(IoUtil.toStream(testStr, CharsetUtil.CHARSET_UTF_8)); + Assert.assertEquals("1dd68d2f119d5640f0d416e99d3f42408b88d511", macHex2); + } +} diff --git a/hutool-crypto/src/test/java/cn/hutool/crypto/test/KeyUtilTest.java b/hutool-crypto/src/test/java/cn/hutool/crypto/test/KeyUtilTest.java new file mode 100644 index 000000000..5ca62af65 --- /dev/null +++ b/hutool-crypto/src/test/java/cn/hutool/crypto/test/KeyUtilTest.java @@ -0,0 +1,26 @@ +package cn.hutool.crypto.test; + +import java.security.KeyPair; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.crypto.CryptoException; +import cn.hutool.crypto.GlobalBouncyCastleProvider; +import cn.hutool.crypto.KeyUtil; + +public class KeyUtilTest { + + /** + * 测试关闭BouncyCastle支持时是否会正常抛出异常,即关闭是否有效 + */ + @Test(expected = CryptoException.class) + @Ignore + public void generateKeyPairTest() { + GlobalBouncyCastleProvider.setUseBouncyCastle(false); + KeyPair pair = KeyUtil.generateKeyPair("SM2"); + Assert.assertNotNull(pair); + } + +} diff --git a/hutool-crypto/src/test/java/cn/hutool/crypto/test/RC4Test.java b/hutool-crypto/src/test/java/cn/hutool/crypto/test/RC4Test.java new file mode 100644 index 000000000..5785182af --- /dev/null +++ b/hutool-crypto/src/test/java/cn/hutool/crypto/test/RC4Test.java @@ -0,0 +1,39 @@ +package cn.hutool.crypto.test; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.crypto.symmetric.RC4; + +public class RC4Test { + + @Test + public void testCryptMessage() { + String key = "This is pretty long key"; + RC4 rc4 = new RC4(key); + String message = "Hello, World!"; + byte[] crypt = rc4.encrypt(message); + String msg = rc4.decrypt(crypt); + Assert.assertEquals(message, msg); + + String message2 = "Hello, World, this is megssage 2"; + byte[] crypt2 = rc4.encrypt(message2); + String msg2 = rc4.decrypt(crypt2); + Assert.assertEquals(message2, msg2); + } + + @Test + public void testCryptWithChineseCharacters() { + String message = "这是一个中文消息!"; + String key = "我是一个文件密钥"; + RC4 rc4 = new RC4(key); + byte[] crypt = rc4.encrypt(message); + String msg = rc4.decrypt(crypt); + Assert.assertEquals(message, msg); + + String message2 = "这是第二个中文消息!"; + byte[] crypt2 = rc4.encrypt(message2); + String msg2 = rc4.decrypt(crypt2); + Assert.assertEquals(message2, msg2); + } +} diff --git a/hutool-crypto/src/test/java/cn/hutool/crypto/test/RSATest.java b/hutool-crypto/src/test/java/cn/hutool/crypto/test/RSATest.java new file mode 100644 index 000000000..8a706225e --- /dev/null +++ b/hutool-crypto/src/test/java/cn/hutool/crypto/test/RSATest.java @@ -0,0 +1,155 @@ +package cn.hutool.crypto.test; + +import java.security.KeyPair; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.HexUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.KeyUtil; +import cn.hutool.crypto.SecureUtil; +import cn.hutool.crypto.asymmetric.KeyType; +import cn.hutool.crypto.asymmetric.RSA; + +/** + * RSA算法单元测试 + * + * @author Looly + * + */ +public class RSATest { + + @Test + public void generateKeyPairTest() { + KeyPair pair = KeyUtil.generateKeyPair("RSA"); + Assert.assertNotNull(pair.getPrivate()); + Assert.assertNotNull(pair.getPublic()); + } + + @Test + public void rsaCustomKeyTest() { + KeyPair pair = KeyUtil.generateKeyPair("RSA"); + byte[] privateKey = pair.getPrivate().getEncoded(); + byte[] publicKey = pair.getPublic().getEncoded(); + + RSA rsa = SecureUtil.rsa(privateKey, publicKey); + + // 公钥加密,私钥解密 + byte[] encrypt = rsa.encrypt(StrUtil.bytes("我是一段测试aaaa", CharsetUtil.CHARSET_UTF_8), KeyType.PublicKey); + byte[] decrypt = rsa.decrypt(encrypt, KeyType.PrivateKey); + Assert.assertEquals("我是一段测试aaaa", StrUtil.str(decrypt, CharsetUtil.CHARSET_UTF_8)); + + // 私钥加密,公钥解密 + byte[] encrypt2 = rsa.encrypt(StrUtil.bytes("我是一段测试aaaa", CharsetUtil.CHARSET_UTF_8), KeyType.PrivateKey); + byte[] decrypt2 = rsa.decrypt(encrypt2, KeyType.PublicKey); + Assert.assertEquals("我是一段测试aaaa", StrUtil.str(decrypt2, CharsetUtil.CHARSET_UTF_8)); + } + + @Test + public void rsaTest() { + final RSA rsa = new RSA(); + + // 获取私钥和公钥 + Assert.assertNotNull(rsa.getPrivateKey()); + Assert.assertNotNull(rsa.getPrivateKeyBase64()); + Assert.assertNotNull(rsa.getPublicKey()); + Assert.assertNotNull(rsa.getPrivateKeyBase64()); + + // 公钥加密,私钥解密 + byte[] encrypt = rsa.encrypt(StrUtil.bytes("我是一段测试aaaa", CharsetUtil.CHARSET_UTF_8), KeyType.PublicKey); + byte[] decrypt = rsa.decrypt(encrypt, KeyType.PrivateKey); + Assert.assertEquals("我是一段测试aaaa", StrUtil.str(decrypt, CharsetUtil.CHARSET_UTF_8)); + + // 私钥加密,公钥解密 + byte[] encrypt2 = rsa.encrypt(StrUtil.bytes("我是一段测试aaaa", CharsetUtil.CHARSET_UTF_8), KeyType.PrivateKey); + byte[] decrypt2 = rsa.decrypt(encrypt2, KeyType.PublicKey); + Assert.assertEquals("我是一段测试aaaa", StrUtil.str(decrypt2, CharsetUtil.CHARSET_UTF_8)); + } + + @Test + public void rsaWithBlockTest2() { + final RSA rsa = new RSA(); + rsa.setEncryptBlockSize(3); + + // 获取私钥和公钥 + Assert.assertNotNull(rsa.getPrivateKey()); + Assert.assertNotNull(rsa.getPrivateKeyBase64()); + Assert.assertNotNull(rsa.getPublicKey()); + Assert.assertNotNull(rsa.getPrivateKeyBase64()); + + // 公钥加密,私钥解密 + byte[] encrypt = rsa.encrypt(StrUtil.bytes("我是一段测试aaaa", CharsetUtil.CHARSET_UTF_8), KeyType.PublicKey); + byte[] decrypt = rsa.decrypt(encrypt, KeyType.PrivateKey); + Assert.assertEquals("我是一段测试aaaa", StrUtil.str(decrypt, CharsetUtil.CHARSET_UTF_8)); + + // 私钥加密,公钥解密 + byte[] encrypt2 = rsa.encrypt(StrUtil.bytes("我是一段测试aaaa", CharsetUtil.CHARSET_UTF_8), KeyType.PrivateKey); + byte[] decrypt2 = rsa.decrypt(encrypt2, KeyType.PublicKey); + Assert.assertEquals("我是一段测试aaaa", StrUtil.str(decrypt2, CharsetUtil.CHARSET_UTF_8)); + } + + @Test + public void rsaBcdTest() { + String text = "我是一段测试aaaa"; + + final RSA rsa = new RSA(); + + // 公钥加密,私钥解密 + String encryptStr = rsa.encryptBcd(text, KeyType.PublicKey); + String decryptStr = StrUtil.utf8Str(rsa.decryptFromBcd(encryptStr, KeyType.PrivateKey)); + Assert.assertEquals(text, decryptStr); + + // 私钥加密,公钥解密 + String encrypt2 = rsa.encryptBcd(text, KeyType.PrivateKey); + String decrypt2 = StrUtil.utf8Str(rsa.decryptFromBcd(encrypt2, KeyType.PublicKey)); + Assert.assertEquals(text, decrypt2); + } + + @Test + public void rsaBase64Test() { + String textBase = "我是一段特别长的测试"; + String text = ""; + for (int i = 0; i < 10; i++) { + text += textBase; + } + + final RSA rsa = new RSA(); + + // 公钥加密,私钥解密 + String encryptStr = rsa.encryptBase64(text, KeyType.PublicKey); + String decryptStr = StrUtil.utf8Str(rsa.decrypt(encryptStr, KeyType.PrivateKey)); + Assert.assertEquals(text, decryptStr); + + // 私钥加密,公钥解密 + String encrypt2 = rsa.encryptBase64(text, KeyType.PrivateKey); + String decrypt2 = StrUtil.utf8Str(rsa.decrypt(encrypt2, KeyType.PublicKey)); + Assert.assertEquals(text, decrypt2); + } + + @Test + public void rsaDecodeTest() { + String PRIVATE_KEY = "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAIL7pbQ+5KKGYRhw7jE31hmA" // + + "f8Q60ybd+xZuRmuO5kOFBRqXGxKTQ9TfQI+aMW+0lw/kibKzaD/EKV91107xE384qOy6IcuBfaR5lv39OcoqNZ"// + + "5l+Dah5ABGnVkBP9fKOFhPgghBknTRo0/rZFGI6Q1UHXb+4atP++LNFlDymJcPAgMBAAECgYBammGb1alndta" // + + "xBmTtLLdveoBmp14p04D8mhkiC33iFKBcLUvvxGg2Vpuc+cbagyu/NZG+R/WDrlgEDUp6861M5BeFN0L9O4hz"// + + "GAEn8xyTE96f8sh4VlRmBOvVdwZqRO+ilkOM96+KL88A9RKdp8V2tna7TM6oI3LHDyf/JBoXaQJBAMcVN7fKlYP" // + + "Skzfh/yZzW2fmC0ZNg/qaW8Oa/wfDxlWjgnS0p/EKWZ8BxjR/d199L3i/KMaGdfpaWbYZLvYENqUCQQCobjsuCW"// + + "nlZhcWajjzpsSuy8/bICVEpUax1fUZ58Mq69CQXfaZemD9Ar4omzuEAAs2/uee3kt3AvCBaeq05NyjAkBme8SwB0iK"// + + "kLcaeGuJlq7CQIkjSrobIqUEf+CzVZPe+AorG+isS+Cw2w/2bHu+G0p5xSYvdH59P0+ZT0N+f9LFAkA6v3Ae56OrI"// + + "wfMhrJksfeKbIaMjNLS9b8JynIaXg9iCiyOHmgkMl5gAbPoH/ULXqSKwzBw5mJ2GW1gBlyaSfV3AkA/RJC+adIjsRGg"// + + "JOkiRjSmPpGv3FOhl9fsBPjupZBEIuoMWOC8GXK/73DHxwmfNmN7C9+sIi4RBcjEeQ5F5FHZ"; + + RSA rsa = new RSA(PRIVATE_KEY, null); + + String a = "2707F9FD4288CEF302C972058712F24A5F3EC62C5A14AD2FC59DAB93503AA0FA17113A020EE4EA35EB53F" // + + "75F36564BA1DABAA20F3B90FD39315C30E68FE8A1803B36C29029B23EB612C06ACF3A34BE815074F5EB5AA3A"// + + "C0C8832EC42DA725B4E1C38EF4EA1B85904F8B10B2D62EA782B813229F9090E6F7394E42E6F44494BB8"; + + byte[] aByte = HexUtil.decodeHex(a); + byte[] decrypt = rsa.decrypt(aByte, KeyType.PrivateKey); + + Assert.assertEquals("虎头闯杭州,多抬头看天,切勿只管种地", StrUtil.str(decrypt, CharsetUtil.CHARSET_UTF_8)); + } +} diff --git a/hutool-crypto/src/test/java/cn/hutool/crypto/test/SM2Test.java b/hutool-crypto/src/test/java/cn/hutool/crypto/test/SM2Test.java new file mode 100644 index 000000000..ec2f393d1 --- /dev/null +++ b/hutool-crypto/src/test/java/cn/hutool/crypto/test/SM2Test.java @@ -0,0 +1,142 @@ +package cn.hutool.crypto.test; + +import java.security.KeyPair; +import java.security.PublicKey; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.HexUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.KeyUtil; +import cn.hutool.crypto.SecureUtil; +import cn.hutool.crypto.SmUtil; +import cn.hutool.crypto.asymmetric.KeyType; +import cn.hutool.crypto.asymmetric.SM2; +import cn.hutool.crypto.asymmetric.SM2Engine.SM2Mode; + +/** + * SM2算法单元测试 + * + * @author Looly, Gsealy + * + */ +public class SM2Test { + + @Test + public void generateKeyPairTest() { + KeyPair pair = SecureUtil.generateKeyPair("SM2"); + Assert.assertNotNull(pair.getPrivate()); + Assert.assertNotNull(pair.getPublic()); + } + + @Test + public void KeyPairOIDTest() { + // OBJECT IDENTIFIER 1.2.156.10197.1.301 + String OID = "06082A811CCF5501822D"; + KeyPair pair = SecureUtil.generateKeyPair("SM2"); + Assert.assertTrue(HexUtil.encodeHexStr(pair.getPrivate().getEncoded()).toUpperCase().contains(OID)); + Assert.assertTrue(HexUtil.encodeHexStr(pair.getPublic().getEncoded()).toUpperCase().contains(OID)); + } + + @Test + public void sm2CustomKeyTest() { + KeyPair pair = SecureUtil.generateKeyPair("SM2"); + byte[] privateKey = pair.getPrivate().getEncoded(); + byte[] publicKey = pair.getPublic().getEncoded(); + + SM2 sm2 = SmUtil.sm2(privateKey, publicKey); + sm2.setMode(SM2Mode.C1C3C2); + + // 公钥加密,私钥解密 + byte[] encrypt = sm2.encrypt(StrUtil.bytes("我是一段测试aaaa", CharsetUtil.CHARSET_UTF_8), KeyType.PublicKey); + byte[] decrypt = sm2.decrypt(encrypt, KeyType.PrivateKey); + Assert.assertEquals("我是一段测试aaaa", StrUtil.str(decrypt, CharsetUtil.CHARSET_UTF_8)); + } + + @Test + public void sm2Test() { + final SM2 sm2 = SmUtil.sm2(); + + // 获取私钥和公钥 + Assert.assertNotNull(sm2.getPrivateKey()); + Assert.assertNotNull(sm2.getPrivateKeyBase64()); + Assert.assertNotNull(sm2.getPublicKey()); + Assert.assertNotNull(sm2.getPrivateKeyBase64()); + + // 公钥加密,私钥解密 + byte[] encrypt = sm2.encrypt(StrUtil.bytes("我是一段测试aaaa", CharsetUtil.CHARSET_UTF_8), KeyType.PublicKey); + byte[] decrypt = sm2.decrypt(encrypt, KeyType.PrivateKey); + Assert.assertEquals("我是一段测试aaaa", StrUtil.str(decrypt, CharsetUtil.CHARSET_UTF_8)); + } + + @Test + public void sm2BcdTest() { + String text = "我是一段测试aaaa"; + + final SM2 sm2 = SmUtil.sm2(); + + // 公钥加密,私钥解密 + String encryptStr = sm2.encryptBcd(text, KeyType.PublicKey); + String decryptStr = StrUtil.utf8Str(sm2.decryptFromBcd(encryptStr, KeyType.PrivateKey)); + Assert.assertEquals(text, decryptStr); + } + + @Test + public void sm2Base64Test() { + String textBase = "我是一段特别长的测试"; + String text = ""; + for (int i = 0; i < 100; i++) { + text += textBase; + } + + final SM2 sm2 = new SM2(); + + // 公钥加密,私钥解密 + String encryptStr = sm2.encryptBase64(text, KeyType.PublicKey); + String decryptStr = StrUtil.utf8Str(sm2.decrypt(encryptStr, KeyType.PrivateKey)); + Assert.assertEquals(text, decryptStr); + } + + @Test + public void sm2SignAndVerifyTest() { + String content = "我是Hanley."; + + final SM2 sm2 = SmUtil.sm2(); + + byte[] sign = sm2.sign(content.getBytes()); + boolean verify = sm2.verify(content.getBytes(), sign); + Assert.assertTrue(verify); + } + + @Test + public void sm2SignAndVerifyUseKeyTest() { + String content = "我是Hanley."; + + KeyPair pair = SecureUtil.generateKeyPair("SM2"); + + final SM2 sm2 = new SM2(// + HexUtil.encodeHexStr(pair.getPrivate().getEncoded()), // + HexUtil.encodeHexStr(pair.getPublic().getEncoded())// + ); + + byte[] sign = sm2.sign(content.getBytes()); + boolean verify = sm2.verify(content.getBytes(), sign); + Assert.assertTrue(verify); + } + + @Test + public void sm2PublicKeyEncodeDecodeTest() { + KeyPair pair = SecureUtil.generateKeyPair("SM2"); + PublicKey publicKey = pair.getPublic(); + byte[] data = KeyUtil.encodeECPublicKey(publicKey); + String encodeHex = HexUtil.encodeHexStr(data); + String encodeB64 = Base64.encode(data); + PublicKey Hexdecode = KeyUtil.decodeECPoint(encodeHex, KeyUtil.SM2_DEFAULT_CURVE); + PublicKey B64decode = KeyUtil.decodeECPoint(encodeB64, KeyUtil.SM2_DEFAULT_CURVE); + Assert.assertEquals(HexUtil.encodeHexStr(publicKey.getEncoded()), HexUtil.encodeHexStr(Hexdecode.getEncoded())); + Assert.assertEquals(HexUtil.encodeHexStr(publicKey.getEncoded()), HexUtil.encodeHexStr(B64decode.getEncoded())); + } +} diff --git a/hutool-crypto/src/test/java/cn/hutool/crypto/test/SignTest.java b/hutool-crypto/src/test/java/cn/hutool/crypto/test/SignTest.java new file mode 100644 index 000000000..d4074554d --- /dev/null +++ b/hutool-crypto/src/test/java/cn/hutool/crypto/test/SignTest.java @@ -0,0 +1,91 @@ +package cn.hutool.crypto.test; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.SecureUtil; +import cn.hutool.crypto.asymmetric.Sign; +import cn.hutool.crypto.asymmetric.SignAlgorithm; + +/** + * 签名单元测试 + * + * @author looly + * + */ +public class SignTest { + + @Test + public void signAndVerifyUseKeyTest() { + String content = "我是Hanley."; + + String privateKey = "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAJ4fG8vJ0tzu7tjXMSJhyNjlE5B7GkTKMKEQlR6LY3IhIhMFVjuA6W+DqH1VMxl9h3GIM4yCKG2VRZEYEPazgVxa5/ifO8W0pfmrzWCPrddUq4t0Slz5u2lLKymLpPjCzboHoDb8VlF+1HOxjKQckAXq9q7U7dV5VxOzJDuZXlz3AgMBAAECgYABo2LfVqT3owYYewpIR+kTzjPIsG3SPqIIWSqiWWFbYlp/BfQhw7EndZ6+Ra602ecYVwfpscOHdx90ZGJwm+WAMkKT4HiWYwyb0ZqQzRBGYDHFjPpfCBxrzSIJ3QL+B8c8YHq4HaLKRKmq7VUF1gtyWaek87rETWAmQoGjt8DyAQJBAOG4OxsT901zjfxrgKwCv6fV8wGXrNfDSViP1t9r3u6tRPsE6Gli0dfMyzxwENDTI75sOEAfyu6xBlemQGmNsfcCQQCzVWQkl9YUoVDWEitvI5MpkvVKYsFLRXKvLfyxLcY3LxpLKBcEeJ/n5wLxjH0GorhJMmM2Rw3hkjUTJCoqqe0BAkATt8FKC0N2O5ryqv1xiUfuxGzW/cX2jzOwDdiqacTuuqok93fKBPzpyhUS8YM2iss7jj6Xs29JzKMOMxK7ZcpfAkAf21lwzrAu9gEgJhYlJhKsXfjJAAYKUwnuaKLs7o65mtp242ZDWxI85eK1+hjzptBJ4HOTXsfufESFY/VBovIBAkAltO886qQRoNSc0OsVlCi4X1DGo6x2RqQ9EsWPrxWEZGYuyEdODrc54b8L+zaUJLfMJdsCIHEUbM7WXxvFVXNv"; + Sign sign = SecureUtil.sign(SignAlgorithm.SHA1withRSA, privateKey, null); + Assert.assertNull(sign.getPublicKeyBase64()); + // 签名 + byte[] signed = sign.sign(content.getBytes()); + + String publicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCeHxvLydLc7u7Y1zEiYcjY5ROQexpEyjChEJUei2NyISITBVY7gOlvg6h9VTMZfYdxiDOMgihtlUWRGBD2s4FcWuf4nzvFtKX5q81gj63XVKuLdEpc+btpSyspi6T4ws26B6A2/FZRftRzsYykHJAF6vau1O3VeVcTsyQ7mV5c9wIDAQAB"; + sign = SecureUtil.sign(SignAlgorithm.SHA1withRSA, null, publicKey); + // 验证签名 + boolean verify = sign.verify(content.getBytes(), signed); + Assert.assertTrue(verify); + } + + @Test + public void signAndVerifyTest() { + signAndVerify(SignAlgorithm.NONEwithRSA); + signAndVerify(SignAlgorithm.MD2withRSA); + signAndVerify(SignAlgorithm.MD5withRSA); + + signAndVerify(SignAlgorithm.SHA1withRSA); + signAndVerify(SignAlgorithm.SHA256withRSA); + signAndVerify(SignAlgorithm.SHA384withRSA); + signAndVerify(SignAlgorithm.SHA512withRSA); + + signAndVerify(SignAlgorithm.NONEwithDSA); + signAndVerify(SignAlgorithm.SHA1withDSA); + + signAndVerify(SignAlgorithm.NONEwithECDSA); + signAndVerify(SignAlgorithm.SHA1withECDSA); + signAndVerify(SignAlgorithm.SHA1withECDSA); + signAndVerify(SignAlgorithm.SHA256withECDSA); + signAndVerify(SignAlgorithm.SHA384withECDSA); + signAndVerify(SignAlgorithm.SHA512withECDSA); + } + + /** + * 测试各种算法的签名和验证签名 + * + * @param signAlgorithm 算法 + */ + private void signAndVerify(SignAlgorithm signAlgorithm) { + byte[] data = StrUtil.utf8Bytes("我是一段测试ab"); + Sign sign = SecureUtil.sign(signAlgorithm); + + // 签名 + byte[] signed = sign.sign(data); + + // 验证签名 + boolean verify = sign.verify(data, signed); + Assert.assertTrue(verify); + } + + /** + * 测试MD5withRSA算法的签名和验证签名 + */ + @Test + public void signAndVerify2() { + String str = "wx2421b1c4370ec43b 支付测试 JSAPI支付测试 10000100 1add1a30ac87aa2db72f57a2375d8fec http://wxpay.wxutil.com/pub_v2/pay/notify.v2.php oUpF8uMuAJO_M2pxb1Q9zNjWeS6o 1415659990 14.23.150.211 1 JSAPI 0CB01533B8C1EF103065174F50BCA001"; + byte[] data = StrUtil.utf8Bytes(str); + Sign sign = SecureUtil.sign(SignAlgorithm.MD5withRSA); + + // 签名 + byte[] signed = sign.sign(data); + + // 验证签名 + boolean verify = sign.verify(data, signed); + Assert.assertTrue(verify); + } +} diff --git a/hutool-crypto/src/test/java/cn/hutool/crypto/test/SmTest.java b/hutool-crypto/src/test/java/cn/hutool/crypto/test/SmTest.java new file mode 100644 index 000000000..340e7377e --- /dev/null +++ b/hutool-crypto/src/test/java/cn/hutool/crypto/test/SmTest.java @@ -0,0 +1,51 @@ +package cn.hutool.crypto.test; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.crypto.SmUtil; +import cn.hutool.crypto.digest.HMac; +import cn.hutool.crypto.symmetric.SymmetricCrypto; + +/** + * SM单元测试 + * + * @author looly + * + */ +public class SmTest { + + @Test + public void sm3Test() { + String digestHex = SmUtil.sm3("aaaaa"); + Assert.assertEquals("136ce3c86e4ed909b76082055a61586af20b4dab674732ebd4b599eef080c9be", digestHex); + } + + @Test + public void sm4Test() { + String content = "test中文"; + SymmetricCrypto sm4 = SmUtil.sm4(); + + String encryptHex = sm4.encryptHex(content); + String decryptStr = sm4.decryptStr(encryptHex, CharsetUtil.CHARSET_UTF_8); + Assert.assertEquals(content, decryptStr); + } + @Test + public void sm4Test2() { + String content = "test中文"; + SymmetricCrypto sm4 = new SymmetricCrypto("SM4/ECB/PKCS5Padding"); + + String encryptHex = sm4.encryptHex(content); + String decryptStr = sm4.decryptStr(encryptHex, CharsetUtil.CHARSET_UTF_8); + Assert.assertEquals(content, decryptStr); + } + + @Test + public void hmacSm3Test() { + String content = "test中文"; + HMac hMac = SmUtil.hmacSm3("password".getBytes()); + String digest = hMac.digestHex(content); + Assert.assertEquals("493e3f9a1896b43075fbe54658076727960d69632ac6b6ed932195857a6840c6", digest); + } +} diff --git a/hutool-crypto/src/test/java/cn/hutool/crypto/test/SymmetricTest.java b/hutool-crypto/src/test/java/cn/hutool/crypto/test/SymmetricTest.java new file mode 100644 index 000000000..8d5960ca7 --- /dev/null +++ b/hutool-crypto/src/test/java/cn/hutool/crypto/test/SymmetricTest.java @@ -0,0 +1,210 @@ +package cn.hutool.crypto.test; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.KeyUtil; +import cn.hutool.crypto.Mode; +import cn.hutool.crypto.Padding; +import cn.hutool.crypto.SecureUtil; +import cn.hutool.crypto.symmetric.AES; +import cn.hutool.crypto.symmetric.DES; +import cn.hutool.crypto.symmetric.DESede; +import cn.hutool.crypto.symmetric.SymmetricAlgorithm; +import cn.hutool.crypto.symmetric.SymmetricCrypto; +import cn.hutool.crypto.symmetric.Vigenere; + +/** + * 对称加密算法单元测试 + * + * @author Looly + * + */ +public class SymmetricTest { + + @Test + public void aesTest() { + String content = "test中文"; + + // 随机生成密钥 + byte[] key = KeyUtil.generateKey(SymmetricAlgorithm.AES.getValue()).getEncoded(); + + // 构建 + SymmetricCrypto aes = new SymmetricCrypto(SymmetricAlgorithm.AES, key); + + // 加密 + byte[] encrypt = aes.encrypt(content); + // 解密 + byte[] decrypt = aes.decrypt(encrypt); + + Assert.assertEquals(content, StrUtil.str(decrypt, CharsetUtil.CHARSET_UTF_8)); + + // 加密为16进制表示 + String encryptHex = aes.encryptHex(content); + // 解密为字符串 + String decryptStr = aes.decryptStr(encryptHex, CharsetUtil.CHARSET_UTF_8); + + Assert.assertEquals(content, decryptStr); + } + + @Test + public void aesTest2() { + String content = "test中文"; + + // 随机生成密钥 + byte[] key = KeyUtil.generateKey(SymmetricAlgorithm.AES.getValue()).getEncoded(); + + // 构建 + AES aes = SecureUtil.aes(key); + + // 加密 + byte[] encrypt = aes.encrypt(content); + // 解密 + byte[] decrypt = aes.decrypt(encrypt); + + Assert.assertEquals(content, StrUtil.utf8Str(decrypt)); + + // 加密为16进制表示 + String encryptHex = aes.encryptHex(content); + // 解密为字符串 + String decryptStr = aes.decryptStr(encryptHex, CharsetUtil.CHARSET_UTF_8); + + Assert.assertEquals(content, decryptStr); + } + + @Test + public void aesTest3() { + String content = "test中文aaaaaaaaaaaaaaaaaaaaa"; + + AES aes = new AES(Mode.CTS, Padding.PKCS5Padding, "0CoJUm6Qyw8W8jud".getBytes(), "0102030405060708".getBytes()); + + // 加密 + byte[] encrypt = aes.encrypt(content); + // 解密 + byte[] decrypt = aes.decrypt(encrypt); + + Assert.assertEquals(content, StrUtil.utf8Str(decrypt)); + + // 加密为16进制表示 + String encryptHex = aes.encryptHex(content); + // 解密为字符串 + String decryptStr = aes.decryptStr(encryptHex, CharsetUtil.CHARSET_UTF_8); + + Assert.assertEquals(content, decryptStr); + } + + @Test + public void aesTest4() { + String content = "4321c9a2db2e6b08987c3b903d8d11ff"; + AES aes = new AES(Mode.CBC, Padding.PKCS5Padding, "0123456789ABHAEQ".getBytes(), "DYgjCEIMVrj2W9xN".getBytes()); + + // 加密为16进制表示 + String encryptHex = aes.encryptHex(content); + + Assert.assertEquals("cd0e3a249eaf0ed80c330338508898c4bddcfd665a1b414622164a273ca5daf7b4ebd2c00aaa66b84dd0a237708dac8e", encryptHex); + } + + @Test + public void desTest() { + String content = "test中文"; + + byte[] key = SecureUtil.generateKey(SymmetricAlgorithm.DES.getValue()).getEncoded(); + + SymmetricCrypto des = new SymmetricCrypto(SymmetricAlgorithm.DES, key); + byte[] encrypt = des.encrypt(content); + byte[] decrypt = des.decrypt(encrypt); + + Assert.assertEquals(content, StrUtil.utf8Str(decrypt)); + + String encryptHex = des.encryptHex(content); + String decryptStr = des.decryptStr(encryptHex); + + Assert.assertEquals(content, decryptStr); + } + + @Test + public void desTest2() { + String content = "test中文"; + + byte[] key = SecureUtil.generateKey(SymmetricAlgorithm.DES.getValue()).getEncoded(); + + DES des = SecureUtil.des(key); + byte[] encrypt = des.encrypt(content); + byte[] decrypt = des.decrypt(encrypt); + + Assert.assertEquals(content, StrUtil.utf8Str(decrypt)); + + String encryptHex = des.encryptHex(content); + String decryptStr = des.decryptStr(encryptHex); + + Assert.assertEquals(content, decryptStr); + } + + @Test + public void desTest3() { + String content = "test中文"; + + DES des = new DES(Mode.CTS, Padding.PKCS5Padding, "0CoJUm6Qyw8W8jud".getBytes(), "01020304".getBytes()); + + byte[] encrypt = des.encrypt(content); + byte[] decrypt = des.decrypt(encrypt); + + Assert.assertEquals(content, StrUtil.utf8Str(decrypt)); + + String encryptHex = des.encryptHex(content); + String decryptStr = des.decryptStr(encryptHex); + + Assert.assertEquals(content, decryptStr); + } + + @Test + public void desdeTest() { + String content = "test中文"; + + byte[] key = SecureUtil.generateKey(SymmetricAlgorithm.DESede.getValue()).getEncoded(); + + DESede des = SecureUtil.desede(key); + + byte[] encrypt = des.encrypt(content); + byte[] decrypt = des.decrypt(encrypt); + + Assert.assertEquals(content, StrUtil.utf8Str(decrypt)); + + String encryptHex = des.encryptHex(content); + String decryptStr = des.decryptStr(encryptHex); + + Assert.assertEquals(content, decryptStr); + } + + @Test + public void desdeTest2() { + String content = "test中文"; + + byte[] key = SecureUtil.generateKey(SymmetricAlgorithm.DESede.getValue()).getEncoded(); + + DESede des = new DESede(Mode.CBC, Padding.PKCS5Padding, key, "12345678".getBytes()); + + byte[] encrypt = des.encrypt(content); + byte[] decrypt = des.decrypt(encrypt); + + Assert.assertEquals(content, StrUtil.utf8Str(decrypt)); + + String encryptHex = des.encryptHex(content); + String decryptStr = des.decryptStr(encryptHex); + + Assert.assertEquals(content, decryptStr); + } + + @Test + public void vigenereTest() { + String content = "Wherethereisawillthereisaway"; + String key = "CompleteVictory"; + + String encrypt = Vigenere.encrypt(content, key); + Assert.assertEquals("zXScRZ]KIOMhQjc0\\bYRXZOJK[Vi", encrypt); + String decrypt = Vigenere.decrypt(encrypt, key); + Assert.assertEquals(content, decrypt); + } +} diff --git a/hutool-crypto/src/test/java/cn/hutool/crypto/test/digest/DigestTest.java b/hutool-crypto/src/test/java/cn/hutool/crypto/test/digest/DigestTest.java new file mode 100644 index 000000000..99fb41262 --- /dev/null +++ b/hutool-crypto/src/test/java/cn/hutool/crypto/test/digest/DigestTest.java @@ -0,0 +1,76 @@ +package cn.hutool.crypto.test.digest; +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.crypto.digest.DigestAlgorithm; +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.crypto.digest.Digester; + +/** + * 摘要算法单元测试 + * @author Looly + * + */ +public class DigestTest { + + @Test + public void digesterTest(){ + String testStr = "test中文"; + + Digester md5 = new Digester(DigestAlgorithm.MD5); + String digestHex = md5.digestHex(testStr); + Assert.assertEquals("5393554e94bf0eb6436f240a4fd71282", digestHex); + } + + @Test + public void md5Test(){ + String testStr = "test中文"; + + String md5Hex1 = DigestUtil.md5Hex(testStr); + Assert.assertEquals("5393554e94bf0eb6436f240a4fd71282", md5Hex1); + + String md5Hex2 = DigestUtil.md5Hex(IoUtil.toStream(testStr, CharsetUtil.CHARSET_UTF_8)); + Assert.assertEquals("5393554e94bf0eb6436f240a4fd71282", md5Hex2); + } + + @Test + public void md5WithSaltTest(){ + String testStr = "test中文"; + + Digester md5 = new Digester(DigestAlgorithm.MD5); + + //加盐 + md5.setSalt("saltTest".getBytes()); + String md5Hex1 = md5.digestHex(testStr); + Assert.assertEquals("762f7335200299dfa09bebbb601a5bc6", md5Hex1); + String md5Hex2 = md5.digestHex(IoUtil.toUtf8Stream(testStr)); + Assert.assertEquals("762f7335200299dfa09bebbb601a5bc6", md5Hex2); + + //重复2次 + md5.setDigestCount(2); + String md5Hex3 = md5.digestHex(testStr); + Assert.assertEquals("2b0616296f6755d25efc07f90afe9684", md5Hex3); + String md5Hex4 = md5.digestHex(IoUtil.toUtf8Stream(testStr)); + Assert.assertEquals("2b0616296f6755d25efc07f90afe9684", md5Hex4); + } + + @Test + public void sha1Test(){ + String testStr = "test中文"; + + String sha1Hex1 = DigestUtil.sha1Hex(testStr); + Assert.assertEquals("ecabf586cef0d3b11c56549433ad50b81110a836", sha1Hex1); + + String sha1Hex2 = DigestUtil.sha1Hex(IoUtil.toStream(testStr, CharsetUtil.CHARSET_UTF_8)); + Assert.assertEquals("ecabf586cef0d3b11c56549433ad50b81110a836", sha1Hex2); + } + + @Test + public void hash256Test() { + String testStr = "Test中文"; + String hex = DigestUtil.sha256Hex(testStr); + Assert.assertEquals(64, hex.length()); + } +} diff --git a/hutool-crypto/src/test/java/cn/hutool/crypto/test/digest/Md5Test.java b/hutool-crypto/src/test/java/cn/hutool/crypto/test/digest/Md5Test.java new file mode 100644 index 000000000..ca586e8fe --- /dev/null +++ b/hutool-crypto/src/test/java/cn/hutool/crypto/test/digest/Md5Test.java @@ -0,0 +1,22 @@ +package cn.hutool.crypto.test.digest; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.crypto.digest.MD5; + +/** + * MD5 单元测试 + * + * @author Looly + * + */ +public class Md5Test { + + @Test + public void md5To16Test() { + String hex16 = new MD5().digestHex16("中国"); + Assert.assertEquals(16, hex16.length()); + Assert.assertEquals("cb143acd6c929826", hex16); + } +} diff --git a/hutool-crypto/src/test/resources/test_private_key.pem b/hutool-crypto/src/test/resources/test_private_key.pem new file mode 100644 index 000000000..1ae3ec62d --- /dev/null +++ b/hutool-crypto/src/test/resources/test_private_key.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQC3G09zGCmlMetvNaWiHbp9d8jItkj5ik0wKcn7jBy/eOdlno5m +y+eijTP/KX8D2QNj2vlF+31/AThYoxI80qUZ3imw8vDVc0cBeGLxEDLVweCQEy7C +ivpkmEWCeyoqThenrwONoIjajG7ZJggFTrsXHL6HsbkGxYrABG2PmQ/w0QIDAQAB +AoGBAIxvTcggSBCC8OciZh6oXlfMfxoxdFavU/QUmO1s0L+pow+1Q9JjoQxy7+ZL +lTcGQitbzsN11xKJhQW2TE6J4EVimJZQSAE4DDmYpMOrkjnBQhkUlaZkkukvDSRS +JqwBI/04G7se+RouHyXjRS9U76HnPM8+/IS2h+T6CbXLOpYBAkEA2j0JmyGVs+WV +I9sG5glamJqTBa4CfTORrdFW4EULoGkUc24ZFFqn9W4e5yfl/pCkPptCenvIrAWp +/ymnHeLn6QJBANbKGO9uBizAt4+o+kHYdANcbU/Cs3PLj8yOOtjkuMbH4tPNQmB6 +/u3npiVk7/Txfkg0BjRzDDZib109eKbvGKkCQBgMneBghRS7+gFng40Z/sfOUOFR +WajeY/FZnk88jJlyuvQ1b8IUc2nSZslmViwFWHQlu9+vgF+kiCU8O9RJSvECQQCl +Vkx7giYerPqgC2MY7JXhQHSkwSuCJ2A6BgImk2npGlTw1UATJJq4Z2jtwBU2Z+7d +ha6BEU6FTqCLFZaaadKBAkEAxko4hrgBsX9BKpFJE3aUIUcMTJfJQdiAhq0k4DV8 +5GVrcp8zl6mUTPZDaOmDhuAjGdAQJqj0Xo0PZ0fOZPtR+w== +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/hutool-crypto/src/test/resources/test_public_key.csr b/hutool-crypto/src/test/resources/test_public_key.csr new file mode 100644 index 000000000..9b67bf230 --- /dev/null +++ b/hutool-crypto/src/test/resources/test_public_key.csr @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICYTCCAcoCCQCs45mePIbzRTANBgkqhkiG9w0BAQUFADB1MQswCQYDVQQGEwJV +UzENMAsGA1UECAwETWFyczETMBEGA1UEBwwKaVRyYW5zd2FycDETMBEGA1UECgwK +aVRyYW5zd2FycDETMBEGA1UECwwKaVRyYW5zd2FycDEYMBYGA1UEAwwPd3d3LjU5 +MXdpZmkuY29tMB4XDTE4MTAxNzAyMTA0OFoXDTI4MTAxNDAyMTA0OFowdTELMAkG +A1UEBhMCVVMxDTALBgNVBAgMBE1hcnMxEzARBgNVBAcMCmlUcmFuc3dhcnAxEzAR +BgNVBAoMCmlUcmFuc3dhcnAxEzARBgNVBAsMCmlUcmFuc3dhcnAxGDAWBgNVBAMM +D3d3dy41OTF3aWZpLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAtxtP +cxgppTHrbzWloh26fXfIyLZI+YpNMCnJ+4wcv3jnZZ6OZsvnoo0z/yl/A9kDY9r5 +Rft9fwE4WKMSPNKlGd4psPLw1XNHAXhi8RAy1cHgkBMuwor6ZJhFgnsqKk4Xp68D +jaCI2oxu2SYIBU67Fxy+h7G5BsWKwARtj5kP8NECAwEAATANBgkqhkiG9w0BAQUF +AAOBgQC2Pko8q1NicJ0oPuhFTPm7n03LtPhCaV/aDf3mqtGxraYifg8iFTxVyZ1c +ol0eEJFsibrQrPEwdSuSVqzwif5Tab9dV92PPFm+Sq0D1Uc0xI4ziXQ+a55K9wrV +TKXxS48TOpnTA8fVFNkUkFNB54Lhh9AwKsx123kJmyaWccbt9Q== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/hutool-db/pom.xml b/hutool-db/pom.xml new file mode 100644 index 000000000..778a5f543 --- /dev/null +++ b/hutool-db/pom.xml @@ -0,0 +1,139 @@ + + + 4.0.0 + + jar + + + cn.hutool + hutool-parent + 4.6.2-SNAPSHOT + + + hutool-db + ${project.artifactId} + Hutool 数据库JDBC的ORM封装 + + + + 0.9.5.4 + 2.4.0 + 8.5.35 + 1.1.12 + 2.4.13 + 3.8.2 + 3.21.0.1 + 2.3.6 + 3.0.1 + + + + + cn.hutool + hutool-core + ${project.parent.version} + + + cn.hutool + hutool-setting + ${project.parent.version} + + + cn.hutool + hutool-log + ${project.parent.version} + + + + + org.apache.tomcat + tomcat-jdbc + ${tomcat-jdbc.version} + true + + + com.alibaba + druid + ${druid.version} + true + + + com.zaxxer + HikariCP-java7 + ${hikariCP.version} + true + + + com.mchange + c3p0 + ${c3p0.version} + true + + + org.apache.commons + commons-dbcp2 + ${dbcp2.version} + true + + + + org.mongodb + mongo-java-driver + ${mongo.version} + true + + + + redis.clients + jedis + ${jedis.version} + true + + + + + org.xerial + sqlite-jdbc + ${sqlite.version} + test + + + org.hsqldb + hsqldb + ${hsqldb.version} + test + + + org.zenframework.z8.dependencies.commons + ojdbc6 + 2.0 + test + + + mysql + mysql-connector-java + 5.1.41 + test + + + org.postgresql + postgresql + 42.2.5.jre7 + test + + + com.microsoft.sqlserver + mssql-jdbc + 6.4.0.jre7 + test + + + org.slf4j + slf4j-simple + 1.7.26 + test + + + diff --git a/hutool-db/src/main/java/cn/hutool/db/AbstractDb.java b/hutool-db/src/main/java/cn/hutool/db/AbstractDb.java new file mode 100644 index 000000000..aea1b21c6 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/AbstractDb.java @@ -0,0 +1,879 @@ +package cn.hutool.db; + +import java.io.Serializable; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Collection; +import java.util.List; + +import javax.sql.DataSource; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.db.dialect.Dialect; +import cn.hutool.db.handler.BeanListHandler; +import cn.hutool.db.handler.EntityHandler; +import cn.hutool.db.handler.EntityListHandler; +import cn.hutool.db.handler.NumberHandler; +import cn.hutool.db.handler.RsHandler; +import cn.hutool.db.handler.StringHandler; +import cn.hutool.db.sql.Condition; +import cn.hutool.db.sql.Condition.LikeType; +import cn.hutool.db.sql.Query; +import cn.hutool.db.sql.SqlExecutor; +import cn.hutool.db.sql.SqlUtil; +import cn.hutool.db.sql.Wrapper; + +/** + * 抽象数据库操作类
+ * 通过给定的数据源执行给定SQL或者给定数据源和方言,执行相应的CRUD操作
+ * 提供抽象方法getConnection和closeConnection,用于自定义数据库连接的打开和关闭 + * + * @author Luxiaolei + * + */ +public abstract class AbstractDb implements Serializable{ + private static final long serialVersionUID = 3858951941916349062L; + + protected DataSource ds; + /** 是否支持事务 */ + protected Boolean isSupportTransaction = null; + protected SqlConnRunner runner; + + // ------------------------------------------------------- Constructor start + /** + * 构造 + * + * @param ds 数据源 + * @param dialect 数据库方言 + */ + public AbstractDb(DataSource ds, Dialect dialect) { + this.ds = ds; + this.runner = new SqlConnRunner(dialect); + } + // ------------------------------------------------------- Constructor end + + /** + * 获得链接。根据实现不同,可以自定义获取连接的方式 + * + * @return {@link Connection} + * @throws SQLException 连接获取异常 + */ + public abstract Connection getConnection() throws SQLException; + + /** + * 关闭连接
+ * 自定义关闭连接有利于自定义回收连接机制,或者不关闭 + * + * @param conn 连接 {@link Connection} + */ + public abstract void closeConnection(Connection conn); + + /** + * 查询 + * + * @param sql 查询语句 + * @param params 参数 + * @return 结果对象 + * @throws SQLException SQL执行异常 + * @since 3.1.1 + */ + public List query(String sql, Object... params) throws SQLException { + return query(sql, new EntityListHandler(), params); + } + + /** + * 查询 + * + * @param 结果集需要处理的对象类型 + * + * @param sql 查询语句 + * @param beanClass 元素Bean类型 + * @param params 参数 + * @return 结果对象 + * @throws SQLException SQL执行异常 + * @since 3.2.2 + */ + public List query(String sql, Class beanClass, Object... params) throws SQLException { + return query(sql, new BeanListHandler(beanClass), params); + } + + /** + * 查询单条记录 + * + * @param sql 查询语句 + * @param params 参数 + * @return 结果对象 + * @throws SQLException SQL执行异常 + */ + public Entity queryOne(String sql, Object... params) throws SQLException { + return query(sql, new EntityHandler(), params); + } + + /** + * 查询单条单个字段记录,并将其转换为Number + * + * @param sql 查询语句 + * @param params 参数 + * @return 结果对象 + * @throws SQLException SQL执行异常 + */ + public Number queryNumber(String sql, Object... params) throws SQLException { + return query(sql, new NumberHandler(), params); + } + + /** + * 查询单条单个字段记录,并将其转换为String + * + * @param sql 查询语句 + * @param params 参数 + * @return 结果对象 + * @throws SQLException SQL执行异常 + */ + public String queryString(String sql, Object... params) throws SQLException { + return query(sql, new StringHandler(), params); + } + + /** + * 查询 + * + * @param 结果集需要处理的对象类型 + * @param sql 查询语句 + * @param rsh 结果集处理对象 + * @param params 参数 + * @return 结果对象 + * @throws SQLException SQL执行异常 + */ + public T query(String sql, RsHandler rsh, Object... params) throws SQLException { + Connection conn = null; + try { + conn = this.getConnection(); + return SqlExecutor.query(conn, sql, rsh, params); + } catch (SQLException e) { + throw e; + } finally { + this.closeConnection(conn); + } + } + + /** + * 执行非查询语句
+ * 语句包括 插入、更新、删除 + * + * @param sql SQL + * @param params 参数 + * @return 影响行数 + * @throws SQLException SQL执行异常 + */ + public int execute(String sql, Object... params) throws SQLException { + Connection conn = null; + try { + conn = this.getConnection(); + return SqlExecutor.execute(conn, sql, params); + } catch (SQLException e) { + throw e; + } finally { + this.closeConnection(conn); + } + } + + /** + * 执行非查询语句
+ * 语句包括 插入、更新、删除 + * + * @param sql SQL + * @param params 参数 + * @return 主键 + * @throws SQLException SQL执行异常 + */ + public Long executeForGeneratedKey(String sql, Object... params) throws SQLException { + Connection conn = null; + try { + conn = this.getConnection(); + return SqlExecutor.executeForGeneratedKey(conn, sql, params); + } catch (SQLException e) { + throw e; + } finally { + this.closeConnection(conn); + } + } + + /** + * 批量执行非查询语句 + * + * @param sql SQL + * @param paramsBatch 批量的参数 + * @return 每个SQL执行影响的行数 + * @throws SQLException SQL执行异常 + */ + public int[] executeBatch(String sql, Object[]... paramsBatch) throws SQLException { + Connection conn = null; + try { + conn = this.getConnection(); + return SqlExecutor.executeBatch(conn, sql, paramsBatch); + } catch (SQLException e) { + throw e; + } finally { + this.closeConnection(conn); + } + } + + /** + * 批量执行非查询语句 + * + * @param sqls SQL列表 + * @return 每个SQL执行影响的行数 + * @throws SQLException SQL执行异常 + * @since 4.5.6 + */ + public int[] executeBatch(String... sqls) throws SQLException { + Connection conn = null; + try { + conn = this.getConnection(); + return SqlExecutor.executeBatch(conn, sqls); + } catch (SQLException e) { + throw e; + } finally { + this.closeConnection(conn); + } + } + + // ---------------------------------------------------------------------------- CRUD start + /** + * 插入数据 + * + * @param record 记录 + * @return 插入行数 + * @throws SQLException SQL执行异常 + */ + public int insert(Entity record) throws SQLException { + Connection conn = null; + try { + conn = this.getConnection(); + return runner.insert(conn, record); + } catch (SQLException e) { + throw e; + } finally { + this.closeConnection(conn); + } + } + + /** + * 插入或更新数据
+ * 根据给定的字段名查询数据,如果存在则更新这些数据,否则执行插入 + * + * @param record 记录 + * @param keys 需要检查唯一性的字段 + * @return 插入行数 + * @throws SQLException SQL执行异常 + * @since 4.0.10 + */ + public int insertOrUpdate(Entity record, String... keys) throws SQLException { + Connection conn = null; + try { + conn = this.getConnection(); + return runner.insertOrUpdate(conn, record, keys); + } catch (SQLException e) { + throw e; + } finally { + this.closeConnection(conn); + } + } + + /** + * 批量插入数据
+ * 需要注意的是,批量插入每一条数据结构必须一致。批量插入数据时会获取第一条数据的字段结构,之后的数据会按照这个格式插入。
+ * 也就是说假如第一条数据只有2个字段,后边数据多于这两个字段的部分将被抛弃。 + * + * @param records 记录列表 + * @return 插入行数 + * @throws SQLException SQL执行异常 + */ + public int[] insert(Collection records) throws SQLException { + Connection conn = null; + try { + conn = this.getConnection(); + return runner.insert(conn, records); + } catch (SQLException e) { + throw e; + } finally { + this.closeConnection(conn); + } + } + + /** + * 插入数据 + * + * @param record 记录 + * @return 主键列表 + * @throws SQLException SQL执行异常 + */ + public List insertForGeneratedKeys(Entity record) throws SQLException { + Connection conn = null; + try { + conn = this.getConnection(); + return runner.insertForGeneratedKeys(conn, record); + } catch (SQLException e) { + throw e; + } finally { + this.closeConnection(conn); + } + } + + /** + * 插入数据 + * + * @param record 记录 + * @return 主键 + * @throws SQLException SQL执行异常 + */ + public Long insertForGeneratedKey(Entity record) throws SQLException { + Connection conn = null; + try { + conn = this.getConnection(); + return runner.insertForGeneratedKey(conn, record); + } catch (SQLException e) { + throw e; + } finally { + this.closeConnection(conn); + } + } + + /** + * 删除数据 + * + * @param tableName 表名 + * @param field 字段名,最好是主键 + * @param value 值,值可以是列表或数组,被当作IN查询处理 + * @return 删除行数 + * @throws SQLException SQL执行异常 + */ + public int del(String tableName, String field, Object value) throws SQLException { + return del(Entity.create(tableName).set(field, value)); + } + + /** + * 删除数据 + * + * @param where 条件 + * @return 影响行数 + * @throws SQLException SQL执行异常 + */ + public int del(Entity where) throws SQLException { + Connection conn = null; + try { + conn = this.getConnection(); + return runner.del(conn, where); + } catch (SQLException e) { + throw e; + } finally { + this.closeConnection(conn); + } + } + + /** + * 更新数据
+ * 更新条件为多个key value对表示,默认key = value,如果使用其它条件可以使用:where.put("key", " > 1"),value也可以传Condition对象,key被忽略 + * + * @param record 记录 + * @param where 条件 + * @return 影响行数 + * @throws SQLException SQL执行异常 + */ + public int update(Entity record, Entity where) throws SQLException { + Connection conn = null; + try { + conn = this.getConnection(); + return runner.update(conn, record, where); + } catch (SQLException e) { + throw e; + } finally { + this.closeConnection(conn); + } + } + + // ------------------------------------------------------------- Get start + /** + * 根据某个字段(最好是唯一字段)查询单个记录
+ * 当有多条返回时,只显示查询到的第一条 + * + * @param 字段值类型 + * @param tableName 表名 + * @param field 字段名 + * @param value 字段值 + * @return 记录 + * @throws SQLException SQL执行异常 + */ + public Entity get(String tableName, String field, T value) throws SQLException { + return this.get(Entity.create(tableName).set(field, value)); + } + + /** + * 根据条件实体查询单个记录,当有多条返回时,只显示查询到的第一条 + * + * @param where 条件 + * @return 记录 + * @throws SQLException SQL执行异常 + */ + public Entity get(Entity where) throws SQLException { + return find(where.getFieldNames(), where, new EntityHandler()); + + } + // ------------------------------------------------------------- Get end + + /** + * 查询
+ * 查询条件为多个key value对表示,默认key = value,如果使用其它条件可以使用:where.put("key", " > 1"),value也可以传Condition对象,key被忽略 + * + * @param 需要处理成的结果对象类型 + * @param fields 返回的字段列表,null则返回所有字段 + * @param where 条件实体类(包含表名) + * @param rsh 结果集处理对象 + * @return 结果对象 + * @throws SQLException SQL执行异常 + */ + public T find(Collection fields, Entity where, RsHandler rsh) throws SQLException { + Connection conn = null; + try { + conn = this.getConnection(); + return runner.find(conn, fields, where, rsh); + } catch (SQLException e) { + throw e; + } finally { + this.closeConnection(conn); + } + } + + /** + * 查询
+ * 查询条件为多个key value对表示,默认key = value,如果使用其它条件可以使用:where.put("key", " > 1"),value也可以传Condition对象,key被忽略 + * + * @param fields 返回的字段列表,null则返回所有字段 + * @param where 条件实体类(包含表名) + * @return 结果Entity列表 + * @return 结果对象 + * @throws SQLException SQL执行异常 + * @since 4.5.16 + */ + public List find(Collection fields, Entity where) throws SQLException { + return find(fields, where, EntityListHandler.create()); + } + + /** + * 查询
+ * Query为查询所需数据的一个实体类,此对象中可以定义返回字段、查询条件,查询的表、分页等信息 + * + * @param 需要处理成的结果对象类型 + * @param query {@link Query}对象,此对象中可以定义返回字段、查询条件,查询的表、分页等信息 + * @param rsh 结果集处理对象 + * @return 结果对象 + * @throws SQLException SQL执行异常 + * @since 4.0.0 + */ + public T find(Query query, RsHandler rsh) throws SQLException { + Connection conn = null; + try { + conn = this.getConnection(); + return runner.find(conn, query, rsh); + } catch (SQLException e) { + throw e; + } finally { + this.closeConnection(conn); + } + } + + /** + * 查询,返回所有字段
+ * 查询条件为多个key value对表示,默认key = value,如果使用其它条件可以使用:where.put("key", " > 1"),value也可以传Condition对象,key被忽略 + * + * @param 需要处理成的结果对象类型 + * @param where 条件实体类(包含表名) + * @param rsh 结果集处理对象 + * @param fields 字段列表,可变长参数如果无值表示查询全部字段 + * @return 结果对象 + * @throws SQLException SQL执行异常 + */ + public T find(Entity where, RsHandler rsh, String... fields) throws SQLException { + return find(CollectionUtil.newArrayList(fields), where, rsh); + } + + /** + * 查询数据列表,返回字段由where参数指定
+ * 查询条件为多个key value对表示,默认key = value,如果使用其它条件可以使用:where.put("key", " > 1"),value也可以传Condition对象,key被忽略 + * + * @param where 条件实体类(包含表名) + * @return 数据对象列表 + * @throws SQLException SQL执行异常 + * @since 3.2.1 + */ + public List find(Entity where) throws SQLException { + return find(where.getFieldNames(), where, EntityListHandler.create()); + } + + /** + * 查询数据列表,返回字段由where参数指定
+ * 查询条件为多个key value对表示,默认key = value,如果使用其它条件可以使用:where.put("key", " > 1"),value也可以传Condition对象,key被忽略 + * + * @param Bean类型 + * @param where 条件实体类(包含表名) + * @return 数据对象列表 + * @throws SQLException SQL执行异常 + * @since 3.2.2 + */ + public List find(Entity where, Class beanClass) throws SQLException { + return find(where.getFieldNames(), where, BeanListHandler.create(beanClass)); + } + + /** + * 查询数据列表,返回所有字段
+ * 查询条件为多个key value对表示,默认key = value,如果使用其它条件可以使用:where.put("key", " > 1"),value也可以传Condition对象,key被忽略 + * + * @param where 条件实体类(包含表名) + * @return 数据对象列表 + * @throws SQLException SQL执行异常 + */ + public List findAll(Entity where) throws SQLException { + return find(where, EntityListHandler.create()); + } + + /** + * 查询数据列表,返回所有字段
+ * 查询条件为多个key value对表示,默认key = value,如果使用其它条件可以使用:where.put("key", " > 1"),value也可以传Condition对象,key被忽略 + * + * @param Bean类型 + * @param where 条件实体类(包含表名) + * @return 数据对象列表 + * @throws SQLException SQL执行异常 + * @since 3.2.2 + */ + public List findAll(Entity where, Class beanClass) throws SQLException { + return find(where, BeanListHandler.create(beanClass)); + } + + /** + * 查询数据列表,返回所有字段 + * + * @param tableName 表名 + * @return 数据对象列表 + * @throws SQLException SQL执行异常 + */ + public List findAll(String tableName) throws SQLException { + return findAll(Entity.create(tableName)); + } + + /** + * 根据某个字段名条件查询数据列表,返回所有字段 + * + * @param tableName 表名 + * @param field 字段名 + * @param value 字段值 + * @return 数据对象列表 + * @throws SQLException SQL执行异常 + */ + public List findBy(String tableName, String field, Object value) throws SQLException { + return findAll(Entity.create(tableName).set(field, value)); + } + + /** + * 根据多个条件查询数据列表,返回所有字段 + * + * @param tableName 表名 + * @param wheres 字段名 + * @return 数据对象列表 + * @throws SQLException SQL执行异常 + * @since 4.0.0 + */ + public List findBy(String tableName, Condition... wheres) throws SQLException { + final Query query = new Query(wheres, tableName); + return find(query, EntityListHandler.create()); + } + + /** + * 根据某个字段名条件查询数据列表,返回所有字段 + * + * @param tableName 表名 + * @param field 字段名 + * @param value 字段值 + * @param likeType {@link LikeType} + * @return 数据对象列表 + * @throws SQLException SQL执行异常 + */ + public List findLike(String tableName, String field, String value, LikeType likeType) throws SQLException { + return findAll(Entity.create(tableName).set(field, SqlUtil.buildLikeValue(value, likeType, true))); + } + + /** + * 结果的条目数 + * + * @param where 查询条件 + * @return 复合条件的结果数 + * @throws SQLException SQL执行异常 + */ + public int count(Entity where) throws SQLException { + Connection conn = null; + try { + conn = this.getConnection(); + return runner.count(conn, where); + } catch (SQLException e) { + throw e; + } finally { + this.closeConnection(conn); + } + } + + /** + * 分页查询
+ * 查询条件为多个key value对表示,默认key = value,如果使用其它条件可以使用:where.put("key", " > 1"),value也可以传Condition对象,key被忽略 + * + * @param 结果对象类型 + * @param fields 返回的字段列表,null则返回所有字段 + * @param where 条件实体类(包含表名) + * @param page 页码,0表示第一页 + * @param numPerPage 每页条目数 + * @param rsh 结果集处理对象 + * @return 结果对象 + * @throws SQLException SQL执行异常 + */ + public T page(Collection fields, Entity where, int page, int numPerPage, RsHandler rsh) throws SQLException { + Connection conn = null; + try { + conn = this.getConnection(); + return runner.page(conn, fields, where, page, numPerPage, rsh); + } catch (SQLException e) { + throw e; + } finally { + this.closeConnection(conn); + } + } + + /** + * 分页查询
+ * 查询条件为多个key value对表示,默认key = value,如果使用其它条件可以使用:where.put("key", " > 1"),value也可以传Condition对象,key被忽略 + * + * @param 结果对象类型 + * @param where 条件实体类(包含表名) + * @param page 页码,0表示第一页 + * @param numPerPage 每页条目数 + * @param rsh 结果集处理对象 + * @return 结果对象 + * @throws SQLException SQL执行异常 + * @since 3.2.2 + */ + public T page(Entity where, int page, int numPerPage, RsHandler rsh) throws SQLException { + return page(where, new Page(page, numPerPage), rsh); + } + + /** + * 分页查询,结果为Entity列表,不计算总数
+ * 查询条件为多个key value对表示,默认key = value,如果使用其它条件可以使用:where.put("key", " > 1"),value也可以传Condition对象,key被忽略 + * + * @param where 条件实体类(包含表名) + * @param page 页码,0表示第一页 + * @param numPerPage 每页条目数 + * @return 结果对象 + * @throws SQLException SQL执行异常 + * @since 3.2.2 + */ + public List pageForEntityList(Entity where, int page, int numPerPage) throws SQLException { + return pageForEntityList(where, new Page(page, numPerPage)); + } + + /** + * 分页查询,结果为Entity列表,不计算总数
+ * 查询条件为多个key value对表示,默认key = value,如果使用其它条件可以使用:where.put("key", " > 1"),value也可以传Condition对象,key被忽略 + * + * @param where 条件实体类(包含表名) + * @param page 分页对象 + * @return 结果对象 + * @throws SQLException SQL执行异常 + * @since 3.2.2 + */ + public List pageForEntityList(Entity where, Page page) throws SQLException { + return page(where, page, EntityListHandler.create()); + } + + /** + * 分页查询
+ * 查询条件为多个key value对表示,默认key = value,如果使用其它条件可以使用:where.put("key", " > 1"),value也可以传Condition对象,key被忽略 + * + * @param 结果对象类型 + * @param where 条件实体类(包含表名) + * @param page 分页对象 + * @param rsh 结果集处理对象 + * @return 结果对象 + * @throws SQLException SQL执行异常 + * @since 3.2.2 + */ + public T page(Entity where, Page page, RsHandler rsh) throws SQLException { + return page(where.getFieldNames(), where, page, rsh); + } + + /** + * 分页查询
+ * 查询条件为多个key value对表示,默认key = value,如果使用其它条件可以使用:where.put("key", " > 1"),value也可以传Condition对象,key被忽略 + * + * @param 结果对象类型 + * @param fields 返回的字段列表,null则返回所有字段 + * @param where 条件实体类(包含表名) + * @param page 分页对象 + * @param rsh 结果集处理对象 + * @return 结果对象 + * @throws SQLException SQL执行异常 + */ + public T page(Collection fields, Entity where, Page page, RsHandler rsh) throws SQLException { + Connection conn = null; + try { + conn = this.getConnection(); + return runner.page(conn, fields, where, page, rsh); + } catch (SQLException e) { + throw e; + } finally { + this.closeConnection(conn); + } + } + + /** + * 分页查询
+ * 查询条件为多个key value对表示,默认key = value,如果使用其它条件可以使用:where.put("key", " > 1"),value也可以传Condition对象,key被忽略 + * + * @param fields 返回的字段列表,null则返回所有字段 + * @param where 条件实体类(包含表名) + * @param page 分页对象 + * @param numPerPage 每页条目数 + * @return 结果对象 + * @throws SQLException SQL执行异常 + */ + public PageResult page(Collection fields, Entity where, int page, int numPerPage) throws SQLException { + Connection conn = null; + try { + conn = this.getConnection(); + return runner.page(conn, fields, where, page, numPerPage); + } catch (SQLException e) { + throw e; + } finally { + this.closeConnection(conn); + } + } + + /** + * 分页查询
+ * 查询条件为多个key value对表示,默认key = value,如果使用其它条件可以使用:where.put("key", " > 1"),value也可以传Condition对象,key被忽略 + * + * @param fields 返回的字段列表,null则返回所有字段 + * @param where 条件实体类(包含表名) + * @param page 分页对象 + * @return 结果对象 + * @throws SQLException SQL执行异常 + */ + public PageResult page(Collection fields, Entity where, Page page) throws SQLException { + Connection conn = null; + try { + conn = this.getConnection(); + return runner.page(conn, fields, where, page); + } catch (SQLException e) { + throw e; + } finally { + this.closeConnection(conn); + } + } + + /** + * 分页查询
+ * 查询条件为多个key value对表示,默认key = value,如果使用其它条件可以使用:where.put("key", " > 1"),value也可以传Condition对象,key被忽略 + * + * @param where 条件实体类(包含表名) + * @param page 页码 + * @param numPerPage 每页条目数 + * @return 分页结果集 + * @throws SQLException SQL执行异常 + * @since 3.2.2 + */ + public PageResult page(Entity where, int page, int numPerPage) throws SQLException { + return this.page(where, new Page(page, numPerPage)); + } + + /** + * 分页查询
+ * 查询条件为多个key value对表示,默认key = value,如果使用其它条件可以使用:where.put("key", " > 1"),value也可以传Condition对象,key被忽略 + * + * @param where 条件实体类(包含表名) + * @param page 分页对象 + * @return 分页结果集 + * @throws SQLException SQL执行异常 + */ + public PageResult page(Entity where, Page page) throws SQLException { + return this.page(where.getFieldNames(), where, page); + } + // ---------------------------------------------------------------------------- CRUD end + + // ---------------------------------------------------------------------------- Getters and Setters start + /** + * 获取{@link SqlConnRunner} + * + * @return {@link SqlConnRunner} + */ + public SqlConnRunner getRunner() { + return runner; + } + + /** + * 设置 {@link SqlConnRunner} + * + * @param runner {@link SqlConnRunner} + */ + public void setRunner(SqlConnRunner runner) { + this.runner = runner; + } + + /** + * 设置包装器,包装器用于对表名、字段名进行符号包装(例如双引号),防止关键字与这些表名或字段冲突 + * + * @param wrapperChar 包装字符,字符会在SQL生成时位于表名和字段名两边,null时表示取消包装 + * @return this + * @since 4.0.0 + */ + public AbstractDb setWrapper(Character wrapperChar) { + return setWrapper(new Wrapper(wrapperChar)); + } + + /** + * 设置包装器,包装器用于对表名、字段名进行符号包装(例如双引号),防止关键字与这些表名或字段冲突 + * + * @param wrapper 包装器,null表示取消包装 + * @return this + * @since 4.0.0 + */ + public AbstractDb setWrapper(Wrapper wrapper) { + this.runner.setWrapper(wrapper); + return this; + } + + /** + * 取消包装器
+ * 取消自动添加到字段名、表名上的包装符(例如双引号) + * + * @return this + * @since 4.5.7 + */ + public AbstractDb disableWrapper() { + return setWrapper((Wrapper) null); + } + // ---------------------------------------------------------------------------- Getters and Setters end + + // ---------------------------------------------------------------------------- protected method start + /** + * 检查数据库是否支持事务,此项检查同一个数据源只检查一次,如果不支持抛出DbRuntimeException异常 + * + * @param conn Connection + * @throws SQLException 获取元数据信息失败 + * @throws DbRuntimeException 不支持事务 + */ + protected void checkTransactionSupported(Connection conn) throws SQLException, DbRuntimeException { + if (null == isSupportTransaction) { + isSupportTransaction = conn.getMetaData().supportsTransactions(); + } + if (false == isSupportTransaction) { + throw new DbRuntimeException("Transaction not supported for current database!"); + } + } + // ---------------------------------------------------------------------------- protected method end +} diff --git a/hutool-db/src/main/java/cn/hutool/db/ActiveEntity.java b/hutool-db/src/main/java/cn/hutool/db/ActiveEntity.java new file mode 100644 index 000000000..a8bd360f0 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/ActiveEntity.java @@ -0,0 +1,232 @@ +package cn.hutool.db; + +import java.sql.SQLException; +import java.util.Collection; + +import cn.hutool.core.map.MapUtil; + +/** + * 动态实体类
+ * 提供了针对自身实体的增删改方法 + * + * @author Looly + * + */ +public class ActiveEntity extends Entity { + private static final long serialVersionUID = 6112321379601134750L; + + private Db db; + + // --------------------------------------------------------------- Static method start + /** + * 创建ActiveEntity + * + * @return ActiveEntity + */ + public static ActiveEntity create() { + return new ActiveEntity(); + } + + /** + * 创建ActiveEntity + * + * @param tableName 表名 + * @return ActiveEntity + */ + public static ActiveEntity create(String tableName) { + return new ActiveEntity(tableName); + } + + /** + * 将PO对象转为Entity + * + * @param Bean对象类型 + * @param bean Bean对象 + * @return ActiveEntity + */ + public static ActiveEntity parse(T bean) { + return create(null).parseBean(bean); + } + + /** + * 将PO对象转为ActiveEntity + * + * @param Bean对象类型 + * @param bean Bean对象 + * @param isToUnderlineCase 是否转换为下划线模式 + * @param ignoreNullValue 是否忽略值为空的字段 + * @return ActiveEntity + */ + public static ActiveEntity parse(T bean, boolean isToUnderlineCase, boolean ignoreNullValue) { + return create(null).parseBean(bean, isToUnderlineCase, ignoreNullValue); + } + + /** + * 将PO对象转为ActiveEntity,并采用下划线法转换字段 + * + * @param Bean对象类型 + * @param bean Bean对象 + * @return ActiveEntity + */ + public static ActiveEntity parseWithUnderlineCase(T bean) { + return create(null).parseBean(bean, true, true); + } + // --------------------------------------------------------------- Static method end + + // -------------------------------------------------------------------------- Constructor start + /** + * 构造 + */ + public ActiveEntity() { + this(Db.use(), (String) null); + } + + /** + * 构造 + * + * @param tableName 表名 + */ + public ActiveEntity(String tableName) { + this(Db.use(), tableName); + } + + /** + * 构造 + * + * @param entity 非动态实体 + */ + public ActiveEntity(Entity entity) { + this(Db.use(), entity); + } + + /** + * 构造 + * + * @param db {@link Db} + * @param tableName 表名 + */ + public ActiveEntity(Db db, String tableName) { + super(tableName); + this.db = db; + } + + /** + * 构造 + * + * @param db {@link Db} + * @param entity 非动态实体 + */ + public ActiveEntity(Db db, Entity entity) { + super(entity.getTableName()); + this.putAll(entity); + this.db = db; + } + // -------------------------------------------------------------------------- Constructor end + + @Override + public ActiveEntity setTableName(String tableName) { + return (ActiveEntity) super.setTableName(tableName); + } + + @Override + public ActiveEntity setFieldNames(Collection fieldNames) { + return (ActiveEntity) super.setFieldNames(fieldNames); + } + + @Override + public ActiveEntity setFieldNames(String... fieldNames) { + return (ActiveEntity) super.setFieldNames(fieldNames); + } + + @Override + public ActiveEntity addFieldNames(String... fieldNames) { + return (ActiveEntity) super.addFieldNames(fieldNames); + } + + @Override + public ActiveEntity parseBean(T bean) { + return (ActiveEntity) super.parseBean(bean); + } + + @Override + public ActiveEntity parseBean(T bean, boolean isToUnderlineCase, boolean ignoreNullValue) { + return (ActiveEntity) super.parseBean(bean, isToUnderlineCase, ignoreNullValue); + } + + @Override + public ActiveEntity set(String field, Object value) { + return (ActiveEntity) super.set(field, value); + } + + @Override + public ActiveEntity setIgnoreNull(String field, Object value) { + return (ActiveEntity) super.setIgnoreNull(field, value); + } + + @Override + public ActiveEntity clone() { + return (ActiveEntity) super.clone(); + } + + // -------------------------------------------------------------------------- CRUD start + /** + * 根据Entity中现有字段条件从数据库中增加一条数据 + * + * @return this + */ + public ActiveEntity add() { + try { + db.insert(this); + } catch (SQLException e) { + throw new DbRuntimeException(e); + } + return this; + } + + /** + * 根据Entity中现有字段条件从数据库中加载一个Entity对象 + * + * @return this + */ + public ActiveEntity load() { + try { + final Entity result = db.get(this); + if(MapUtil.isNotEmpty(result)) { + this.putAll(result); + } + } catch (SQLException e) { + throw new DbRuntimeException(e); + } + return this; + } + + /** + * 根据现有Entity中的条件删除与之匹配的数据库记录 + * + * @return this + */ + public ActiveEntity del() { + try { + db.del(this); + } catch (SQLException e) { + throw new DbRuntimeException(e); + } + return this; + } + + /** + * 根据现有Entity中的条件删除与之匹配的数据库记录 + * + * @param primaryKey 主键名 + * @return this + */ + public ActiveEntity update(String primaryKey) { + try { + db.update(this, Entity.create().set(primaryKey, this.get(primaryKey))); + } catch (SQLException e) { + throw new DbRuntimeException(e); + } + return this; + } + // -------------------------------------------------------------------------- CRUD end +} diff --git a/hutool-db/src/main/java/cn/hutool/db/DaoTemplate.java b/hutool-db/src/main/java/cn/hutool/db/DaoTemplate.java new file mode 100644 index 000000000..ea8865dad --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/DaoTemplate.java @@ -0,0 +1,361 @@ +package cn.hutool.db; + +import java.sql.SQLException; +import java.util.Arrays; +import java.util.List; + +import javax.sql.DataSource; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.db.ds.DSFactory; +import cn.hutool.db.handler.EntityHandler; +import cn.hutool.db.handler.EntityListHandler; + +/** + * 数据访问层模板
+ * 此模板用于简化对指定表的操作,简化的操作如下:
+ * 1、在初始化时指定了表名,CRUD操作时便不需要表名
+ * 2、在初始化时指定了主键,某些需要主键的操作便不需要指定主键类型 + * @author Looly + * + */ +public class DaoTemplate { + + /** 表名 */ + protected String tableName; + /** 本表的主键字段,请在子类中覆盖或构造方法中指定,默认为id */ + protected String primaryKeyField = "id"; + /** SQL运行器 */ + protected Db db; + + //--------------------------------------------------------------- Constructor start + /** + * 构造,此构造需要自定义SqlRunner,主键默认为id + * @param tableName 数据库表名 + */ + public DaoTemplate(String tableName) { + this(tableName, (String)null); + } + + /** + * 构造,使用默认的池化连接池,读取默认配置文件的空分组,适用于只有一个数据库的情况 + * @param tableName 数据库表名 + * @param primaryKeyField 主键字段名 + */ + public DaoTemplate(String tableName, String primaryKeyField) { + this(tableName, primaryKeyField, DSFactory.get()); + } + + public DaoTemplate(String tableName, DataSource ds) { + this(tableName, null, ds); + } + + /** + * 构造 + * @param tableName 表名 + * @param primaryKeyField 主键字段名 + * @param ds 数据源 + */ + public DaoTemplate(String tableName, String primaryKeyField, DataSource ds) { + this(tableName, primaryKeyField, Db.use(ds)); + } + + /** + * 构造 + * @param tableName 表名 + * @param primaryKeyField 主键字段名 + * @param db Db对象 + */ + public DaoTemplate(String tableName, String primaryKeyField, Db db) { + this.tableName = tableName; + if(StrUtil.isNotBlank(primaryKeyField)){ + this.primaryKeyField = primaryKeyField; + } + this.db = db; + } + //--------------------------------------------------------------- Constructor end + + //------------------------------------------------------------- Add start + /** + * 添加 + * @param entity 实体对象 + * @return 插入行数 + * @throws SQLException SQL执行异常 + */ + public int add(Entity entity) throws SQLException { + return db.insert(fixEntity(entity)); + } + + /** + * 添加 + * @param entity 实体对象 + * @return 主键列表 + * @throws SQLException SQL执行异常 + */ + public List addForGeneratedKeys(Entity entity) throws SQLException { + return db.insertForGeneratedKeys(fixEntity(entity)); + } + + /** + * 添加 + * @param entity 实体对象 + * @return 自增主键 + * @throws SQLException SQL执行异常 + */ + public Long addForGeneratedKey(Entity entity) throws SQLException { + return db.insertForGeneratedKey(fixEntity(entity)); + } + //------------------------------------------------------------- Add end + + //------------------------------------------------------------- Delete start + /** + * 删除 + * @param 主键类型 + * + * @param pk 主键 + * @return 删除行数 + * @throws SQLException SQL执行异常 + */ + public int del(T pk) throws SQLException { + if (pk == null) { + return 0; + } + return this.del(Entity.create(tableName).set(primaryKeyField, pk)); + } + + /** + * 删除 + * + * @param 主键类型 + * @param field 字段名 + * @param value 字段值 + * @return 删除行数 + * @throws SQLException SQL执行异常 + */ + public int del(String field, T value) throws SQLException { + if (StrUtil.isBlank(field)) { + return 0; + } + + return this.del(Entity.create(tableName).set(field, value)); + } + + /** + * 删除 + * + * @param 主键类型 + * @param where 删除条件,当条件为空时,返回0(防止误删全表) + * @return 删除行数 + * @throws SQLException SQL执行异常 + */ + public int del(Entity where) throws SQLException { + if (CollectionUtil.isEmpty(where)) { + return 0; + } + return db.del(fixEntity(where)); + } + //------------------------------------------------------------- Delete end + + //------------------------------------------------------------- Update start + /** + * 按照条件更新 + * @param record 更新的内容 + * @param where 条件 + * @return 更新条目数 + * @throws SQLException SQL执行异常 + */ + public int update(Entity record, Entity where) throws SQLException{ + if (CollectionUtil.isEmpty(record)) { + return 0; + } + return db.update(fixEntity(record), where); + } + + /** + * 更新 + * @param entity 实体对象,必须包含主键 + * @return 更新行数 + * @throws SQLException SQL执行异常 + */ + public int update(Entity entity) throws SQLException { + if (CollectionUtil.isEmpty(entity)) { + return 0; + } + entity = fixEntity(entity); + Object pk = entity.get(primaryKeyField); + if (null == pk) { + throw new SQLException(StrUtil.format("Please determine `{}` for update", primaryKeyField)); + } + + final Entity where = Entity.create(tableName).set(primaryKeyField, pk); + final Entity record = (Entity) entity.clone(); + record.remove(primaryKeyField); + + return db.update(record, where); + } + + /** + * 增加或者更新实体 + * @param entity 实体,当包含主键时更新,否则新增 + * @return 新增或更新条数 + * @throws SQLException SQL执行异常 + */ + public int addOrUpdate(Entity entity) throws SQLException { + return null == entity.get(primaryKeyField) ? add(entity) : update(entity); + } + //------------------------------------------------------------- Update end + + //------------------------------------------------------------- Get start + /** + * 根据主键获取单个记录 + * + * @param 主键类型 + * @param pk 主键值 + * @return 记录 + * @throws SQLException SQL执行异常 + */ + public Entity get(T pk) throws SQLException { + return this.get(primaryKeyField, pk); + } + + /** + * 根据某个字段(最好是唯一字段)查询单个记录
+ * 当有多条返回时,只显示查询到的第一条 + * + * @param 字段值类型 + * @param field 字段名 + * @param value 字段值 + * @return 记录 + * @throws SQLException SQL执行异常 + */ + public Entity get(String field, T value) throws SQLException { + return this.get(Entity.create(tableName).set(field, value)); + } + + /** + * 根据条件实体查询单个记录,当有多条返回时,只显示查询到的第一条 + * + * @param where 条件 + * @return 记录 + * @throws SQLException SQL执行异常 + */ + public Entity get(Entity where) throws SQLException { + return db.find(null, fixEntity(where), new EntityHandler()); + } + //------------------------------------------------------------- Get end + + //------------------------------------------------------------- Find start + /** + * 根据某个字段值查询结果 + * + * @param 字段值类型 + * @param field 字段名 + * @param value 字段值 + * @return 记录 + * @throws SQLException SQL执行异常 + */ + public List find(String field, T value) throws SQLException { + return this.find(Entity.create(tableName).set(field, value)); + } + + /** + * 查询当前表的所有记录 + * @return 记录 + * @throws SQLException SQL执行异常 + */ + public List findAll() throws SQLException { + return this.find(Entity.create(tableName)); + } + + /** + * 根据某个字段值查询结果 + * + * @param where 查询条件 + * @return 记录 + * @throws SQLException SQL执行异常 + */ + public List find(Entity where) throws SQLException { + return db.find(null, fixEntity(where), new EntityListHandler()); + } + + /** + * 根据SQL语句查询结果
+ * SQL语句可以是非完整SQL语句,可以只提供查询的条件部分(例如WHERE部分)
+ * 此方法会自动补全SELECT * FROM [tableName] 部分,这样就无需关心表名,直接提供条件即可 + * + * @param sql SQL语句 + * @param params SQL占位符中对应的参数 + * @return 记录 + * @throws SQLException SQL执行异常 + */ + public List findBySql(String sql, Object... params) throws SQLException { + String selectKeyword = StrUtil.subPre(sql.trim(), 6).toLowerCase(); + if(false == "select".equals(selectKeyword)){ + sql = "SELECT * FROM " + this.tableName + " " + sql; + } + return db.query(sql, new EntityListHandler(), params); + } + + /** + * 分页 + * + * @param where 条件 + * @param page 分页对象 + * @param selectFields 查询的字段列表 + * @return 分页结果集 + * @throws SQLException SQL执行异常 + */ + public PageResult page(Entity where, Page page, String... selectFields) throws SQLException{ + return db.page(Arrays.asList(selectFields), fixEntity(where), page); + } + + /** + * 分页 + * + * @param where 条件 + * @param page 分页对象 + * @return 分页结果集 + * @throws SQLException SQL执行异常 + */ + public PageResult page(Entity where, Page page) throws SQLException{ + return db.page(fixEntity(where), page); + } + + /** + * 满足条件的数据条目数量 + * + * @param where 条件 + * @return 数量 + * @throws SQLException SQL执行异常 + */ + public int count(Entity where) throws SQLException{ + return db.count(fixEntity(where)); + } + + /** + * 指定条件的数据是否存在 + * + * @param where 条件 + * @return 是否存在 + * @throws SQLException SQL执行异常 + */ + public boolean exist(Entity where) throws SQLException{ + return this.count(where) > 0; + } + //------------------------------------------------------------- Find end + + /** + * 修正Entity对象,避免null和填充表名 + * @param entity 实体类 + * @return 修正后的实体类 + */ + private Entity fixEntity(Entity entity){ + if(null == entity){ + entity = Entity.create(tableName); + }else if(StrUtil.isBlank(entity.getTableName())){ + entity.setTableName(tableName); + } + return entity; + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/Db.java b/hutool-db/src/main/java/cn/hutool/db/Db.java new file mode 100644 index 000000000..41459ffaf --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/Db.java @@ -0,0 +1,239 @@ +package cn.hutool.db; + +import java.sql.Connection; +import java.sql.SQLException; + +import javax.sql.DataSource; + +import cn.hutool.core.lang.func.VoidFunc1; +import cn.hutool.db.dialect.Dialect; +import cn.hutool.db.dialect.DialectFactory; +import cn.hutool.db.ds.DSFactory; +import cn.hutool.db.sql.Wrapper; +import cn.hutool.db.transaction.TransactionLevel; +import cn.hutool.log.StaticLog; + +/** + * 数据库操作类
+ * 通过给定的数据源执行给定SQL或者给定数据源和方言,执行相应的CRUD操作
+ * + * @author Looly + * @since 4.1.2 + */ +public class Db extends AbstractDb { + private static final long serialVersionUID = -3378415769645309514L; + + /** + * 创建Db
+ * 使用默认数据源,自动探测数据库连接池 + * + * @return Db + */ + public static Db use() { + return use(DSFactory.get()); + } + + /** + * 创建Db
+ * 使用默认数据源,自动探测数据库连接池 + * + * @param group 数据源分组 + * @return Db + */ + public static Db use(String group) { + return use(DSFactory.get(group)); + } + + /** + * 创建Db
+ * 会根据数据源连接的元信息识别目标数据库类型,进而使用合适的数据源 + * + * @param ds 数据源 + * @return Db + */ + public static Db use(DataSource ds) { + return ds == null ? null : new Db(ds); + } + + /** + * 创建Db + * + * @param ds 数据源 + * @param dialect 方言 + * @return Db + */ + public static Db use(DataSource ds, Dialect dialect) { + return new Db(ds, dialect); + } + + /** + * 创建Db + * + * @param ds 数据源 + * @param driverClassName 数据库连接驱动类名 + * @return Db + */ + public static Db use(DataSource ds, String driverClassName) { + return new Db(ds, DialectFactory.newDialect(driverClassName)); + } + + // ---------------------------------------------------------------------------- Constructor start + /** + * 构造,从DataSource中识别方言 + * + * @param ds 数据源 + */ + public Db(DataSource ds) { + this(ds, DialectFactory.getDialect(ds)); + } + + /** + * 构造 + * + * @param ds 数据源 + * @param driverClassName 数据库连接驱动类名,用于识别方言 + */ + public Db(DataSource ds, String driverClassName) { + this(ds, DialectFactory.newDialect(driverClassName)); + } + + /** + * 构造 + * + * @param ds 数据源 + * @param dialect 方言 + */ + public Db(DataSource ds, Dialect dialect) { + super(ds, dialect); + } + // ---------------------------------------------------------------------------- Constructor end + + // ---------------------------------------------------------------------------- Getters and Setters start + @Override + public Db setWrapper(Character wrapperChar) { + return (Db) super.setWrapper(wrapperChar); + } + + @Override + public Db setWrapper(Wrapper wrapper) { + return (Db) super.setWrapper(wrapper); + } + + @Override + public Db disableWrapper() { + return (Db)super.disableWrapper(); + } + // ---------------------------------------------------------------------------- Getters and Setters end + + @Override + public Connection getConnection() throws SQLException { + return ThreadLocalConnection.INSTANCE.get(this.ds); + } + + @Override + public void closeConnection(Connection conn) { + try { + if (conn != null && false == conn.getAutoCommit()) { + // 事务中的Session忽略关闭事件 + return; + } + } catch (SQLException e) { + // ignore + } + + ThreadLocalConnection.INSTANCE.close(this.ds); + } + + /** + * 执行事务,使用默认的事务级别
+ * 在同一事务中,所有对数据库操作都是原子的,同时提交或者同时回滚 + * + * @param func 事务函数,所有操作应在同一函数下执行,确保在同一事务中 + * @return this + * @throws SQLException SQL异常 + */ + public Db tx(VoidFunc1 func) throws SQLException { + return tx(null, func); + } + + /** + * 执行事务
+ * 在同一事务中,所有对数据库操作都是原子的,同时提交或者同时回滚 + * + * @param transactionLevel 事务级别枚举,null表示使用JDBC默认事务 + * @param func 事务函数,所有操作应在同一函数下执行,确保在同一事务中 + * @return this + * @throws SQLException SQL异常 + */ + public Db tx(TransactionLevel transactionLevel, VoidFunc1 func) throws SQLException { + final Connection conn = getConnection(); + + // 检查是否支持事务 + checkTransactionSupported(conn); + + // 设置事务级别 + if (null != transactionLevel) { + final int level = transactionLevel.getLevel(); + if (conn.getTransactionIsolation() < level) { + // 用户定义的事务级别如果比默认级别更严格,则按照严格的级别进行 + conn.setTransactionIsolation(level); + } + } + + // 开始事务 + boolean autoCommit = conn.getAutoCommit(); + if (autoCommit) { + conn.setAutoCommit(false); + } + + // 执行事务 + try { + func.call(this); + // 提交 + conn.commit(); + } catch (Throwable e) { + quietRollback(conn); + throw (e instanceof SQLException) ? (SQLException) e : new SQLException(e); + } finally { + // 还原事务状态 + quietSetAutoCommit(conn, autoCommit); + // 关闭连接或将连接归还连接池 + closeConnection(conn); + } + + return this; + } + + // ---------------------------------------------------------------------------- Private method start + /** + * 静默回滚事务 + * + * @param conn Connection + */ + private void quietRollback(Connection conn) { + if (null != conn) { + try { + conn.rollback(); + } catch (Exception e) { + StaticLog.error(e); + } + } + } + + /** + * 静默设置自动提交 + * + * @param conn Connection + * @param autoCommit 是否自动提交 + */ + private void quietSetAutoCommit(Connection conn, Boolean autoCommit) { + if (null != autoCommit) { + try { + conn.setAutoCommit(autoCommit); + } catch (Exception e) { + StaticLog.error(e); + } + } + } + // ---------------------------------------------------------------------------- Private method end +} \ No newline at end of file diff --git a/hutool-db/src/main/java/cn/hutool/db/DbRuntimeException.java b/hutool-db/src/main/java/cn/hutool/db/DbRuntimeException.java new file mode 100644 index 000000000..544219ed2 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/DbRuntimeException.java @@ -0,0 +1,32 @@ +package cn.hutool.db; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 数据库异常 + * @author xiaoleilu + */ +public class DbRuntimeException extends RuntimeException{ + private static final long serialVersionUID = 3624487785708765623L; + + public DbRuntimeException(Throwable e) { + super(ExceptionUtil.getMessage(e), e); + } + + public DbRuntimeException(String message) { + super(message); + } + + public DbRuntimeException(String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params)); + } + + public DbRuntimeException(String message, Throwable throwable) { + super(message, throwable); + } + + public DbRuntimeException(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/DbUtil.java b/hutool-db/src/main/java/cn/hutool/db/DbUtil.java new file mode 100644 index 000000000..6e34706cb --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/DbUtil.java @@ -0,0 +1,262 @@ +package cn.hutool.db; + +import java.io.Closeable; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.sql.DataSource; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.io.IoUtil; +import cn.hutool.db.dialect.Dialect; +import cn.hutool.db.dialect.DialectFactory; +import cn.hutool.db.ds.DSFactory; +import cn.hutool.db.sql.SqlLog; +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import cn.hutool.log.level.Level; +import cn.hutool.setting.Setting; + +/** + * 数据库操作工具类 + * + * @author Luxiaolei + * + */ +public final class DbUtil { + private final static Log log = LogFactory.get(); + + /** + * 实例化一个新的SQL运行对象 + * + * @param dialect 数据源 + * @return SQL执行类 + */ + public static SqlConnRunner newSqlConnRunner(Dialect dialect) { + return SqlConnRunner.create(dialect); + } + + /** + * 实例化一个新的SQL运行对象 + * + * @param ds 数据源 + * @return SQL执行类 + */ + public static SqlConnRunner newSqlConnRunner(DataSource ds) { + return SqlConnRunner.create(ds); + } + + /** + * 实例化一个新的SQL运行对象 + * + * @param conn 数据库连接对象 + * @return SQL执行类 + */ + public static SqlConnRunner newSqlConnRunner(Connection conn) { + return SqlConnRunner.create(DialectFactory.newDialect(conn)); + } + + /** + * 实例化一个新的SQL运行对象,使用默认数据源 + * + * @return SQL执行类 + * @deprecated 请使用 {@link #use()} + */ + @Deprecated + public static SqlRunner newSqlRunner() { + return SqlRunner.create(getDs()); + } + + /** + * 实例化一个新的SQL运行对象 + * + * @param ds 数据源 + * @return SQL执行类 + * @deprecated 请使用 {@link #use(DataSource)} + */ + @Deprecated + public static SqlRunner newSqlRunner(DataSource ds) { + return SqlRunner.create(ds); + } + + /** + * 实例化一个新的SQL运行对象 + * + * @param ds 数据源 + * @param dialect SQL方言 + * @return SQL执行类 + * @deprecated 请使用 {@link #use(DataSource, Dialect)} + */ + @Deprecated + public static SqlRunner newSqlRunner(DataSource ds, Dialect dialect) { + return SqlRunner.create(ds, dialect); + } + + /** + * 实例化一个新的Db,使用默认数据源 + * + * @return SQL执行类 + */ + public static Db use() { + return Db.use(); + } + + /** + * 实例化一个新的Db对象 + * + * @param ds 数据源 + * @return SQL执行类 + */ + public static Db use(DataSource ds) { + return Db.use(ds); + } + + /** + * 实例化一个新的SQL运行对象 + * + * @param ds 数据源 + * @param dialect SQL方言 + * @return SQL执行类 + */ + public static Db use(DataSource ds, Dialect dialect) { + return Db.use(ds, dialect); + } + + /** + * 新建数据库会话,使用默认数据源 + * + * @return 数据库会话 + */ + public static Session newSession() { + return Session.create(getDs()); + } + + /** + * 新建数据库会话 + * + * @param ds 数据源 + * @return 数据库会话 + */ + public static Session newSession(DataSource ds) { + return Session.create(ds); + } + + /** + * 连续关闭一系列的SQL相关对象
+ * 这些对象必须按照顺序关闭,否则会出错。 + * + * @param objsToClose 需要关闭的对象 + */ + public static void close(Object... objsToClose) { + for (Object obj : objsToClose) { + if (obj instanceof AutoCloseable) { + IoUtil.close((AutoCloseable) obj); + } else if (obj instanceof Closeable) { + IoUtil.close((Closeable) obj); + } else { + try { + if (obj != null) { + if (obj instanceof ResultSet) { + ((ResultSet) obj).close(); + } else if (obj instanceof Statement) { + ((Statement) obj).close(); + } else if (obj instanceof PreparedStatement) { + ((PreparedStatement) obj).close(); + } else if (obj instanceof Connection) { + ((Connection) obj).close(); + } else { + log.warn("Object {} not a ResultSet or Statement or PreparedStatement or Connection!", obj.getClass().getName()); + } + } + } catch (SQLException e) { + // ignore + } + } + } + } + + /** + * 获得默认数据源 + * + * @return 默认数据源 + */ + public static DataSource getDs() { + return DSFactory.get(); + } + + /** + * 获取指定分组的数据源 + * + * @param group 分组 + * @return 数据源 + */ + public static DataSource getDs(String group) { + return DSFactory.get(group); + } + + /** + * 获得JNDI数据源 + * + * @param jndiName JNDI名称 + * @return 数据源 + */ + public static DataSource getJndiDsWithLog(String jndiName) { + try { + return getJndiDs(jndiName); + } catch (DbRuntimeException e) { + log.error(e.getCause(), "Find JNDI datasource error!"); + } + return null; + } + + /** + * 获得JNDI数据源 + * + * @param jndiName JNDI名称 + * @return 数据源 + */ + public static DataSource getJndiDs(String jndiName) { + try { + return (DataSource) new InitialContext().lookup(jndiName); + } catch (NamingException e) { + throw new DbRuntimeException(e); + } + } + + /** + * 从配置文件中读取SQL打印选项 + * @param setting 配置文件 + * @since 4.1.7 + */ + public static void setShowSqlGlobal(Setting setting) { + // 初始化SQL显示 + final boolean isShowSql = Convert.toBool(setting.remove("showSql"), false); + final boolean isFormatSql = Convert.toBool(setting.remove("formatSql"), false); + final boolean isShowParams = Convert.toBool(setting.remove("showParams"), false); + String sqlLevelStr = setting.remove("sqlLevel"); + if (null != sqlLevelStr) { + sqlLevelStr = sqlLevelStr.toUpperCase(); + } + final Level level = Convert.toEnum(Level.class, sqlLevelStr, Level.DEBUG); + log.debug("Show sql: [{}], format sql: [{}], show params: [{}], level: [{}]", isShowSql, isFormatSql, isShowParams, level); + setShowSqlGlobal(isShowSql, isFormatSql, isShowParams, level); + } + + /** + * 设置全局配置:是否通过debug日志显示SQL + * + * @param isShowSql 是否显示SQL + * @param isFormatSql 是否格式化显示的SQL + * @param isShowParams 是否打印参数 + * @param level SQL打印到的日志等级 + * @since 4.1.7 + */ + public static void setShowSqlGlobal(boolean isShowSql, boolean isFormatSql, boolean isShowParams, Level level) { + SqlLog.INSTASNCE.init(isShowSql, isFormatSql, isShowParams, level); + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/Entity.java b/hutool-db/src/main/java/cn/hutool/db/Entity.java new file mode 100644 index 000000000..85cdf2db1 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/Entity.java @@ -0,0 +1,397 @@ +package cn.hutool.db; + +import java.nio.charset.Charset; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.RowId; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Collection; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Dict; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.db.sql.SqlUtil; + +/** + * 数据实体对象
+ * 数据实体类充当两个角色:
+ * 1. 数据的载体,一个Entity对应数据库中的一个row
+ * 2. SQL条件,Entity中的每一个字段对应一个条件,字段值对应条件的值 + * + * @author loolly + * + */ +public class Entity extends Dict { + private static final long serialVersionUID = -1951012511464327448L; + + // --------------------------------------------------------------- Static method start + /** + * 创建Entity + * + * @return Entity + */ + public static Entity create() { + return new Entity(); + } + + /** + * 创建Entity + * + * @param tableName 表名 + * @return Entity + */ + public static Entity create(String tableName) { + return new Entity(tableName); + } + + /** + * 将PO对象转为Entity + * + * @param Bean对象类型 + * @param bean Bean对象 + * @return Entity + */ + public static Entity parse(T bean) { + return create(null).parseBean(bean); + } + + /** + * 将PO对象转为Entity + * + * @param Bean对象类型 + * @param bean Bean对象 + * @param isToUnderlineCase 是否转换为下划线模式 + * @param ignoreNullValue 是否忽略值为空的字段 + * @return Entity + */ + public static Entity parse(T bean, boolean isToUnderlineCase, boolean ignoreNullValue) { + return create(null).parseBean(bean, isToUnderlineCase, ignoreNullValue); + } + + /** + * 将PO对象转为Entity,并采用下划线法转换字段 + * + * @param Bean对象类型 + * @param bean Bean对象 + * @return Entity + */ + public static Entity parseWithUnderlineCase(T bean) { + return create(null).parseBean(bean, true, true); + } + // --------------------------------------------------------------- Static method end + + /* 表名 */ + private String tableName; + /* 字段名列表,用于限制加入的字段的值 */ + private Set fieldNames; + + // --------------------------------------------------------------- Constructor start + public Entity() { + super(); + } + + /** + * 构造 + * + * @param tableName 数据表名 + */ + + public Entity(String tableName) { + super(); + this.tableName = tableName; + } + + /** + * 构造 + * + * @param tableName 数据表名 + * @param caseInsensitive 是否大小写不敏感 + * @since 4.5.16 + */ + public Entity(String tableName, boolean caseInsensitive) { + super(caseInsensitive); + this.tableName = tableName; + } + // --------------------------------------------------------------- Constructor end + + // --------------------------------------------------------------- Getters and Setters start + /** + * @return 获得表名 + */ + public String getTableName() { + return tableName; + } + + /** + * 设置表名 + * + * @param tableName 表名 + * @return 本身 + */ + public Entity setTableName(String tableName) { + this.tableName = tableName; + return this; + } + + /** + * + * @return 字段集合 + */ + public Set getFieldNames() { + return this.fieldNames; + } + + /** + * 设置字段列表,用于限制加入的字段的值 + * + * @param fieldNames 字段列表 + * @return 自身 + */ + public Entity setFieldNames(Collection fieldNames) { + if (CollectionUtil.isNotEmpty(fieldNames)) { + this.fieldNames = new HashSet(fieldNames); + } + return this; + } + + /** + * 设置字段列表,用于限制加入的字段的值 + * + * @param fieldNames 字段列表 + * @return 自身 + */ + public Entity setFieldNames(String... fieldNames) { + if (ArrayUtil.isNotEmpty(fieldNames)) { + this.fieldNames = CollectionUtil.newHashSet(fieldNames); + } + return this; + } + + /** + * 添加字段列表 + * + * @param fieldNames 字段列表 + * @return 自身 + */ + public Entity addFieldNames(String... fieldNames) { + if (ArrayUtil.isNotEmpty(fieldNames)) { + if (null == this.fieldNames) { + return setFieldNames(fieldNames); + } else { + for (String fieldName : fieldNames) { + this.fieldNames.add(fieldName); + } + } + } + return this; + } + + // --------------------------------------------------------------- Getters and Setters end + /** + * 将值对象转换为Entity
+ * 类名会被当作表名,小写第一个字母 + * + * @param Bean对象类型 + * @param bean Bean对象 + * @return 自己 + */ + @Override + public Entity parseBean(T bean) { + if (StrUtil.isBlank(this.tableName)) { + this.setTableName(StrUtil.lowerFirst(bean.getClass().getSimpleName())); + } + return (Entity) super.parseBean(bean); + } + + /** + * 将值对象转换为Entity
+ * 类名会被当作表名,小写第一个字母 + * + * @param Bean对象类型 + * @param bean Bean对象 + * @param isToUnderlineCase 是否转换为下划线模式 + * @param ignoreNullValue 是否忽略值为空的字段 + * @return 自己 + */ + @Override + public Entity parseBean(T bean, boolean isToUnderlineCase, boolean ignoreNullValue) { + if (StrUtil.isBlank(this.tableName)) { + String simpleName = bean.getClass().getSimpleName(); + this.setTableName(isToUnderlineCase ? StrUtil.toUnderlineCase(simpleName) : StrUtil.lowerFirst(simpleName)); + } + return (Entity) super.parseBean(bean, isToUnderlineCase, ignoreNullValue); + } + + /** + * 过滤Map保留指定键值对,如果键不存在跳过 + * + * @param keys 键列表 + * @return Dict 结果 + * @since 4.0.10 + */ + public Entity filter(String... keys) { + final Entity result = new Entity(this.tableName); + result.setFieldNames(this.fieldNames); + + for (String key : keys) { + if(this.containsKey(key)) { + result.put(key, this.get(key)); + } + } + return result; + } + + // -------------------------------------------------------------------- Put and Set start + @Override + public Entity set(String field, Object value) { + return (Entity) super.set(field, value); + } + + @Override + public Entity setIgnoreNull(String field, Object value) { + return (Entity) super.setIgnoreNull(field, value); + } + // -------------------------------------------------------------------- Put and Set end + + // -------------------------------------------------------------------- Get start + + /** + * 获得Clob类型结果 + * + * @param field 参数 + * @return Clob + */ + public Clob getClob(String field) { + return get(field, null); + } + + /** + * 获得Blob类型结果 + * + * @param field 参数 + * @return Blob + * @since 3.0.6 + */ + public Blob getBlob(String field) { + return get(field, null); + } + + @Override + public Time getTime(String field) { + Object obj = get(field); + Time result = null; + if (null != obj) { + try { + result = (Time) obj; + } catch (Exception e) { + // try oracle.sql.TIMESTAMP + result = ReflectUtil.invoke(obj, "timeValue"); + } + } + return result; + } + + @Override + public Date getDate(String field) { + Object obj = get(field); + Date result = null; + if (null != obj) { + try { + result = (Date) obj; + } catch (Exception e) { + // try oracle.sql.TIMESTAMP + result = ReflectUtil.invoke(obj, "dateValue"); + } + } + return result; + } + + @Override + public Timestamp getTimestamp(String field) { + Object obj = get(field); + Timestamp result = null; + if (null != obj) { + try { + result = (Timestamp) obj; + } catch (Exception e) { + // try oracle.sql.TIMESTAMP + result = ReflectUtil.invoke(obj, "timestampValue"); + } + } + return result; + } + + @Override + public String getStr(String field) { + return getStr(field, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 获得字符串值
+ * 支持Clob、Blob、RowId + * + * @param field 字段名 + * @param charset 编码 + * @return 字段对应值 + * @since 3.0.6 + */ + public String getStr(String field, Charset charset) { + final Object obj = get(field); + if (obj instanceof Clob) { + return SqlUtil.clobToStr((Clob) obj); + } else if (obj instanceof Blob) { + return SqlUtil.blobToStr((Blob) obj, charset); + } else if (obj instanceof RowId) { + final RowId rowId = (RowId) obj; + return StrUtil.str(rowId.getBytes(), charset); + } + return super.getStr(field); + } + + /** + * 获得rowid + * + * @return RowId + */ + public RowId getRowId() { + return getRowId("ROWID"); + } + + /** + * 获得rowid + * + * @param field rowid属性名 + * @return RowId + */ + public RowId getRowId(String field) { + Object obj = this.get(field); + if (null == obj) { + return null; + } + if (obj instanceof RowId) { + return (RowId) obj; + } + throw new DbRuntimeException("Value of field [{}] is not a rowid!", field); + } + + // -------------------------------------------------------------------- Get end + + // -------------------------------------------------------------------- 特殊方法 start + @Override + public Entity clone() { + return (Entity) super.clone(); + } + // -------------------------------------------------------------------- 特殊方法 end + + @Override + public String toString() { + return "Entity {tableName=" + tableName + ", fieldNames=" + fieldNames + ", fields=" + super.toString() + "}"; + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/Page.java b/hutool-db/src/main/java/cn/hutool/db/Page.java new file mode 100644 index 000000000..3f51ee4a3 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/Page.java @@ -0,0 +1,175 @@ +package cn.hutool.db; + +import java.io.Serializable; +import java.util.Arrays; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.PageUtil; +import cn.hutool.db.sql.Order; + +/** + * 分页对象 + * + * @author Looly + * + */ +public class Page implements Serializable { + private static final long serialVersionUID = 97792549823353462L; + + public static final int DEFAULT_PAGE_SIZE = 20; + + /** 页码,0表示第一页 */ + private int pageNumber; + /** 每页结果数 */ + private int pageSize; + /** 排序 */ + private Order[] orders; + + // ---------------------------------------------------------- Constructor start + /** + * 构造,默认第0页,每页{@value #DEFAULT_PAGE_SIZE} 条 + * + * @since 4.5.16 + */ + public Page() { + this(0, DEFAULT_PAGE_SIZE); + } + + /** + * 构造 + * + * @param pageNumber 页码,0表示第一页 + * @param pageSize 每页结果数 + */ + public Page(int pageNumber, int pageSize) { + this.pageNumber = pageNumber < 0 ? 0 : pageNumber; + this.pageSize = pageSize <= 0 ? DEFAULT_PAGE_SIZE : pageSize; + } + + /** + * 构造 + * + * @param pageNumber 页码 + * @param numPerPage 每页结果数 + * @param order 排序对象 + */ + public Page(int pageNumber, int numPerPage, Order order) { + this(pageNumber, numPerPage); + this.orders = new Order[] { order }; + } + // ---------------------------------------------------------- Constructor start + + // ---------------------------------------------------------- Getters and Setters start + /** + * @return 页码 + */ + public int getPageNumber() { + return pageNumber; + } + + /** + * 设置页码,0表示第一页 + * + * @param pageNumber 页码 + */ + public void setPageNumber(int pageNumber) { + this.pageNumber = pageNumber < 0 ? 0 : pageNumber; + } + + /** + * @return 每页结果数 + * @deprecated 使用 {@link #getPageSize()} 代替 + */ + @Deprecated + public int getNumPerPage() { + return getPageSize(); + } + + /** + * 设置每页结果数 + * + * @param pageSize 每页结果数 + * @deprecated 使用 {@link #setPageSize(int)} 代替 + */ + @Deprecated + public void setNumPerPage(int pageSize) { + setPageSize(pageSize); + } + + /** + * @return 每页结果数 + */ + public int getPageSize() { + return pageSize; + } + + /** + * 设置每页结果数 + * + * @param pageSize 每页结果数 + */ + public void setPageSize(int pageSize) { + this.pageSize = pageSize <= 0 ? DEFAULT_PAGE_SIZE : pageSize; + } + + /** + * @return 排序 + */ + public Order[] getOrders() { + return this.orders; + } + + /** + * 设置排序 + * + * @param orders 排序 + */ + public void setOrder(Order... orders) { + this.orders = orders; + } + + /** + * 设置排序 + * + * @param orders 排序 + */ + public void addOrder(Order... orders) { + if (null != this.orders) { + ArrayUtil.append(this.orders, orders); + } + this.orders = orders; + } + // ---------------------------------------------------------- Getters and Setters end + + /** + * @return 开始位置 + */ + public int getStartPosition() { + return getStartEnd()[0]; + } + + /** + * @return 结束位置 + */ + public int getEndPosition() { + return getStartEnd()[1]; + } + + /** + * 开始位置和结束位置
+ * 例如:
+ * 页码:1,每页10 =》 [0, 10]
+ * 页码:2,每页10 =》 [10, 20]
+ * 。。。
+ * + * @return 第一个数为开始位置,第二个数为结束位置 + */ + public int[] getStartEnd() { + return PageUtil.transToStartEnd(pageNumber, pageSize); + } + + @Override + public String toString() { + return "Page [page=" + pageNumber + ", pageSize=" + pageSize + ", order=" + Arrays.toString(orders) + "]"; + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/PageResult.java b/hutool-db/src/main/java/cn/hutool/db/PageResult.java new file mode 100644 index 000000000..0ec4f303d --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/PageResult.java @@ -0,0 +1,143 @@ +package cn.hutool.db; + +import java.util.ArrayList; + +import cn.hutool.core.util.PageUtil; + +/** + * 分页数据结果集 + * @author Looly + * + * @param 结果集项的类型 + */ +public class PageResult extends ArrayList{ + private static final long serialVersionUID = 9056411043515781783L; + + public static final int DEFAULT_PAGE_SIZE = Page.DEFAULT_PAGE_SIZE; + + /** 页码 */ + private int page; + /** 每页结果数 */ + private int pageSize; + /** 总页数 */ + private int totalPage; + /** 总数 */ + private int total; + + //---------------------------------------------------------- Constructor start + /** + * 构造 + * @param page 页码 + * @param pageSize 每页结果数 + */ + public PageResult(int page, int pageSize) { + super(pageSize <= 0 ? DEFAULT_PAGE_SIZE : pageSize); + + this.page = page <= 0 ? 0 : page; + this.pageSize = pageSize <= 0 ? DEFAULT_PAGE_SIZE : pageSize; + } + + /** + * 构造 + * @param page 页码 + * @param pageSize 每页结果数 + * @param total 结果总数 + */ + public PageResult(int page, int pageSize, int total) { + this(page, pageSize); + + this.total = total; + this.totalPage = PageUtil.totalPage(total,pageSize); + } + //---------------------------------------------------------- Constructor end + + //---------------------------------------------------------- Getters and Setters start + /** + * @return 页码 + */ + public int getPage() { + return page; + } + /** + * 设置页码 + * @param page 页码 + */ + public void setPage(int page) { + this.page = page; + } + + /** + * @return 每页结果数 + * @deprecated 请使用{@link #getPageSize()} + */ + @Deprecated + public int getNumPerPage() { + return pageSize; + } + /** + * 设置每页结果数 + * @param pageSize 每页结果数 + * @deprecated 请使用 {@link #setPageSize(int)} + */ + @Deprecated + public void setNumPerPage(int pageSize) { + this.pageSize = pageSize; + } + + /** + * @return 每页结果数 + */ + public int getPageSize() { + return pageSize; + } + /** + * 设置每页结果数 + * @param pageSize 每页结果数 + */ + public void setPageSize(int pageSize) { + this.pageSize = pageSize; + } + + /** + * @return 总页数 + */ + public int getTotalPage() { + return totalPage; + } + /** + * 设置总页数 + * @param totalPage 总页数 + */ + public void setTotalPage(int totalPage) { + this.totalPage = totalPage; + } + + /** + * @return 总数 + */ + public int getTotal() { + return total; + } + /** + * 设置总数 + * @param total 总数 + */ + public void setTotal(int total) { + this.total = total; + } + //---------------------------------------------------------- Getters and Setters end + + /** + * @return 是否第一页 + */ + public boolean isFirst(){ + return this.page == 0; + } + + /** + * @return 是否最后一页 + */ + public boolean isLast() { + return this.page >= this.totalPage; + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/Session.java b/hutool-db/src/main/java/cn/hutool/db/Session.java new file mode 100644 index 000000000..1039dbf0a --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/Session.java @@ -0,0 +1,333 @@ +package cn.hutool.db; + +import java.io.Closeable; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Savepoint; + +import javax.sql.DataSource; + +import cn.hutool.core.lang.func.VoidFunc0; +import cn.hutool.core.lang.func.VoidFunc1; +import cn.hutool.core.util.StrUtil; +import cn.hutool.db.dialect.Dialect; +import cn.hutool.db.dialect.DialectFactory; +import cn.hutool.db.ds.DSFactory; +import cn.hutool.db.sql.Wrapper; +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; + +/** + * 数据库SQL执行会话
+ * 会话通过共用Connection而可以实现JDBC事务
+ * 一个会话只维护一个连接,推荐在执行完后关闭Session,避免重用
+ * 本对象并不是线程安全的,多个线程共用一个Session将会导致不可预知的问题 + * + * @author loolly + * + */ +public class Session extends AbstractDb implements Closeable { + private static final long serialVersionUID = 3421251905539056945L; + private final static Log log = LogFactory.get(); + + /** + * 创建默认数据源会话 + * + * @return {@link Session} + * @since 3.2.3 + */ + public static Session create() { + return new Session(DSFactory.get()); + } + + /** + * 创建会话 + * + * @param group 分组 + * @return {@link Session} + * @since 4.0.11 + */ + public static Session create(String group) { + return new Session(DSFactory.get(group)); + } + + /** + * 创建会话 + * + * @param ds 数据源 + * @return {@link Session} + */ + public static Session create(DataSource ds) { + return new Session(ds); + } + + // ---------------------------------------------------------------------------- Constructor start + /** + * 构造,从DataSource中识别方言 + * + * @param ds 数据源 + */ + public Session(DataSource ds) { + this(ds, DialectFactory.getDialect(ds)); + } + + /** + * 构造 + * + * @param ds 数据源 + * @param driverClassName 数据库连接驱动类名,用于识别方言 + */ + public Session(DataSource ds, String driverClassName) { + this(ds, DialectFactory.newDialect(driverClassName)); + } + + /** + * 构造 + * + * @param ds 数据源 + * @param dialect 方言 + */ + public Session(DataSource ds, Dialect dialect) { + super(ds, dialect); + } + // ---------------------------------------------------------------------------- Constructor end + + // ---------------------------------------------------------------------------- Getters and Setters end + /** + * 获得{@link SqlConnRunner} + * + * @return {@link SqlConnRunner} + */ + public SqlConnRunner getRunner() { + return runner; + } + // ---------------------------------------------------------------------------- Getters and Setters end + + // ---------------------------------------------------------------------------- Transaction method start + /** + * 开始事务 + * + * @throws SQLException SQL执行异常 + */ + public void beginTransaction() throws SQLException { + final Connection conn = getConnection(); + checkTransactionSupported(conn); + conn.setAutoCommit(false); + } + + /** + * 提交事务 + * + * @throws SQLException SQL执行异常 + */ + public void commit() throws SQLException { + try { + getConnection().commit(); + } catch (SQLException e) { + throw e; + } finally { + try { + getConnection().setAutoCommit(true); // 事务结束,恢复自动提交 + } catch (SQLException e) { + log.error(e); + } + } + } + + /** + * 回滚事务 + * + * @throws SQLException SQL执行异常 + */ + public void rollback() throws SQLException { + try { + getConnection().rollback(); + } catch (SQLException e) { + throw e; + } finally { + try { + getConnection().setAutoCommit(true); // 事务结束,恢复自动提交 + } catch (SQLException e) { + log.error(e); + } + } + } + + /** + * 静默回滚事务
+ * 回滚事务 + */ + public void quietRollback() { + try { + getConnection().rollback(); + } catch (Exception e) { + log.error(e); + } finally { + try { + getConnection().setAutoCommit(true); // 事务结束,恢复自动提交 + } catch (SQLException e) { + log.error(e); + } + } + } + + /** + * 回滚到某个保存点,保存点的设置请使用setSavepoint方法 + * + * @param savepoint 保存点 + * @throws SQLException SQL执行异常 + */ + public void rollback(Savepoint savepoint) throws SQLException { + try { + getConnection().rollback(savepoint); + } catch (SQLException e) { + throw e; + } finally { + try { + getConnection().setAutoCommit(true); // 事务结束,恢复自动提交 + } catch (SQLException e) { + log.error(e); + } + } + } + + /** + * 静默回滚到某个保存点,保存点的设置请使用setSavepoint方法 + * + * @param savepoint 保存点 + * @throws SQLException SQL执行异常 + */ + public void quietRollback(Savepoint savepoint) throws SQLException { + try { + getConnection().rollback(savepoint); + } catch (Exception e) { + log.error(e); + } finally { + try { + getConnection().setAutoCommit(true); // 事务结束,恢复自动提交 + } catch (SQLException e) { + log.error(e); + } + } + } + + /** + * 设置保存点 + * + * @return 保存点对象 + * @throws SQLException SQL执行异常 + */ + public Savepoint setSavepoint() throws SQLException { + return getConnection().setSavepoint(); + } + + /** + * 设置保存点 + * + * @param name 保存点的名称 + * @return 保存点对象 + * @throws SQLException SQL执行异常 + */ + public Savepoint setSavepoint(String name) throws SQLException { + return getConnection().setSavepoint(name); + } + + /** + * 设置事务的隔离级别
+ * + * Connection.TRANSACTION_NONE 驱动不支持事务
+ * Connection.TRANSACTION_READ_UNCOMMITTED 允许脏读、不可重复读和幻读
+ * Connection.TRANSACTION_READ_COMMITTED 禁止脏读,但允许不可重复读和幻读
+ * Connection.TRANSACTION_REPEATABLE_READ 禁止脏读和不可重复读,单运行幻读
+ * Connection.TRANSACTION_SERIALIZABLE 禁止脏读、不可重复读和幻读
+ * + * @param level 隔离级别 + * @throws SQLException SQL执行异常 + */ + public void setTransactionIsolation(int level) throws SQLException { + if (getConnection().getMetaData().supportsTransactionIsolationLevel(level) == false) { + throw new SQLException(StrUtil.format("Transaction isolation [{}] not support!", level)); + } + getConnection().setTransactionIsolation(level); + } + + /** + * 在事务中执行操作,通过实现{@link VoidFunc0}接口的call方法执行多条SQL语句从而完成事务 + * + * @param func 函数抽象,在函数中执行多个SQL操作,多个操作会被合并为同一事务 + * @throws SQLException + * @since 3.2.3 + */ + public void tx(VoidFunc1 func) throws SQLException { + try { + beginTransaction(); + func.call(this); + commit(); + } catch (Throwable e) { + quietRollback(); + throw (e instanceof SQLException) ? (SQLException) e : new SQLException(e); + } + } + + /** + * 在事务中执行操作,通过实现{@link VoidFunc0}接口的call方法执行多条SQL语句从而完成事务 + * + * @param func 函数抽象,在函数中执行多个SQL操作,多个操作会被合并为同一事务 + * @since 3.2.3 + * @deprecated 请使用{@link #tx(VoidFunc1)} + */ + @Deprecated + public void trans(VoidFunc1 func) { + try { + beginTransaction(); + func.call(this); + commit(); + } catch (Exception e) { + quietRollback(); + throw new DbRuntimeException(e); + } + } + // ---------------------------------------------------------------------------- Transaction method end + + // ---------------------------------------------------------------------------- Getters and Setters start + @Override + public Session setWrapper(Character wrapperChar) { + return (Session) super.setWrapper(wrapperChar); + } + + @Override + public Session setWrapper(Wrapper wrapper) { + return (Session) super.setWrapper(wrapper); + } + + @Override + public Session disableWrapper() { + return (Session) super.disableWrapper(); + } + // ---------------------------------------------------------------------------- Getters and Setters end + + @Override + public Connection getConnection() throws SQLException { + return ThreadLocalConnection.INSTANCE.get(this.ds); + } + + @Override + public void closeConnection(Connection conn) { + try { + if(conn != null && false == conn.getAutoCommit()) { + // 事务中的Session忽略关闭事件 + return; + } + } catch (SQLException e) { + log.error(e); + } + + // 普通请求关闭(或归还)连接 + ThreadLocalConnection.INSTANCE.close(this.ds); + } + + @Override + public void close() { + closeConnection(null); + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/SqlConnRunner.java b/hutool-db/src/main/java/cn/hutool/db/SqlConnRunner.java new file mode 100644 index 000000000..3cfb5c945 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/SqlConnRunner.java @@ -0,0 +1,604 @@ +package cn.hutool.db; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Collection; +import java.util.List; + +import javax.sql.DataSource; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.db.dialect.Dialect; +import cn.hutool.db.dialect.DialectFactory; +import cn.hutool.db.handler.EntityListHandler; +import cn.hutool.db.handler.NumberHandler; +import cn.hutool.db.handler.PageResultHandler; +import cn.hutool.db.handler.RsHandler; +import cn.hutool.db.sql.Condition.LikeType; +import cn.hutool.db.sql.Query; +import cn.hutool.db.sql.SqlExecutor; +import cn.hutool.db.sql.SqlUtil; +import cn.hutool.db.sql.Wrapper; + +/** + * SQL执行类
+ * 此执行类只接受方言参数,不需要数据源,只有在执行方法时需要数据库连接对象
+ * 此对象存在的意义在于,可以由使用者自定义数据库连接对象,并执行多个方法,方便事务的统一控制或减少连接对象的创建关闭 + * @author Luxiaolei + * + */ +public class SqlConnRunner{ + private Dialect dialect; + + /** + * 实例化一个新的SQL运行对象 + * + * @param dialect 方言 + * @return SQL执行类 + */ + public static SqlConnRunner create(Dialect dialect) { + return new SqlConnRunner(dialect); + } + + /** + * 实例化一个新的SQL运行对象 + * + * @param ds 数据源 + * @return SQL执行类 + */ + public static SqlConnRunner create(DataSource ds) { + return new SqlConnRunner(DialectFactory.getDialect(ds)); + } + + /** + * 实例化一个新的SQL运行对象 + * + * @param driverClassName 驱动类名 + * @return SQL执行类 + */ + public static SqlConnRunner create(String driverClassName) { + return new SqlConnRunner(driverClassName); + } + + //------------------------------------------------------- Constructor start + /** + * 构造 + * @param dialect 方言 + */ + public SqlConnRunner(Dialect dialect) { + this.dialect = dialect; + } + + /** + * 构造 + * @param driverClassName 驱动类名,,用于识别方言 + */ + public SqlConnRunner(String driverClassName) { + this(DialectFactory.newDialect(driverClassName)); + } + //------------------------------------------------------- Constructor end + + //---------------------------------------------------------------------------- CRUD start + /** + * 插入数据
+ * 此方法不会关闭Connection + * @param conn 数据库连接 + * @param record 记录 + * @return 插入行数 + * @throws SQLException SQL执行异常 + */ + public int insert(Connection conn, Entity record) throws SQLException { + checkConn(conn); + if(CollectionUtil.isEmpty(record)){ + throw new SQLException("Empty entity provided!"); + } + PreparedStatement ps = null; + try { + ps = dialect.psForInsert(conn, record); + return ps.executeUpdate(); + } catch (SQLException e) { + throw e; + } finally { + DbUtil.close(ps); + } + } + + /** + * 插入数据
+ * 此方法不会关闭Connection + * + * @param conn 数据库连接 + * @param record 记录 + * @param keys 需要检查唯一性的字段 + * @return 插入行数 + * @throws SQLException SQL执行异常 + */ + public int insertOrUpdate(Connection conn, Entity record, String... keys) throws SQLException { + final Entity where = record.filter(keys); + if(MapUtil.isNotEmpty(where) && count(conn, where) > 0) { + return update(conn, record, where); + }else { + return insert(conn, record); + } + } + + /** + * 批量插入数据
+ * 需要注意的是,批量插入每一条数据结构必须一致。批量插入数据时会获取第一条数据的字段结构,之后的数据会按照这个格式插入。
+ * 也就是说假如第一条数据只有2个字段,后边数据多于这两个字段的部分将被抛弃。 + * 此方法不会关闭Connection + * + * @param conn 数据库连接 + * @param records 记录列表,记录KV必须严格一致 + * @return 插入行数 + * @throws SQLException SQL执行异常 + */ + public int[] insert(Connection conn, Collection records) throws SQLException { + return insert(conn, records.toArray(new Entity[records.size()])); + } + + /** + * 批量插入数据
+ * 批量插入必须严格保持Entity的结构一致,不一致会导致插入数据出现不可预知的结果
+ * 此方法不会关闭Connection + * + * @param conn 数据库连接 + * @param records 记录列表,记录KV必须严格一致 + * @return 插入行数 + * @throws SQLException SQL执行异常 + */ + public int[] insert(Connection conn, Entity... records) throws SQLException { + checkConn(conn); + if(ArrayUtil.isEmpty(records)){ + return new int[]{0}; + } + + //单条单独处理 + if(1 == records.length) { + return new int[] { insert(conn, records[0])}; + } + + PreparedStatement ps = null; + try { + ps = dialect.psForInsertBatch(conn, records); + return ps.executeBatch(); + } finally { + DbUtil.close(ps); + } + } + + /** + * 插入数据
+ * 此方法不会关闭Connection + * @param conn 数据库连接 + * @param record 记录 + * @return 主键列表 + * @throws SQLException SQL执行异常 + */ + public List insertForGeneratedKeys(Connection conn, Entity record) throws SQLException { + checkConn(conn); + if(CollectionUtil.isEmpty(record)){ + throw new SQLException("Empty entity provided!"); + } + + PreparedStatement ps = null; + try { + ps = dialect.psForInsert(conn, record); + ps.executeUpdate(); + return StatementUtil.getGeneratedKeys(ps); + } catch (SQLException e) { + throw e; + } finally { + DbUtil.close(ps); + } + } + + /** + * 插入数据
+ * 此方法不会关闭Connection + * @param conn 数据库连接 + * @param record 记录 + * @return 自增主键 + * @throws SQLException SQL执行异常 + */ + public Long insertForGeneratedKey(Connection conn, Entity record) throws SQLException { + checkConn(conn); + if(CollectionUtil.isEmpty(record)){ + throw new SQLException("Empty entity provided!"); + } + + PreparedStatement ps = null; + try { + ps = dialect.psForInsert(conn, record); + ps.executeUpdate(); + return StatementUtil.getGeneratedKeyOfLong(ps); + } catch (SQLException e) { + throw e; + } finally { + DbUtil.close(ps); + } + } + + /** + * 删除数据
+ * 此方法不会关闭Connection + * @param conn 数据库连接 + * @param where 条件 + * @return 影响行数 + * @throws SQLException SQL执行异常 + */ + public int del(Connection conn, Entity where) throws SQLException { + checkConn(conn); + if(CollectionUtil.isEmpty(where)){ + //不允许做全表删除 + throw new SQLException("Empty entity provided!"); + } + + final Query query = new Query(SqlUtil.buildConditions(where), where.getTableName()); + PreparedStatement ps = null; + try { + ps = dialect.psForDelete(conn, query); + return ps.executeUpdate(); + } catch (SQLException e) { + throw e; + } finally { + DbUtil.close(ps); + } + } + + /** + * 更新数据
+ * 此方法不会关闭Connection + * @param conn 数据库连接 + * @param record 记录 + * @param where 条件 + * @return 影响行数 + * @throws SQLException SQL执行异常 + */ + public int update(Connection conn, Entity record, Entity where) throws SQLException { + checkConn(conn); + if(CollectionUtil.isEmpty(record)){ + throw new SQLException("Empty entity provided!"); + } + if(CollectionUtil.isEmpty(where)){ + //不允许做全表更新 + throw new SQLException("Empty where provided!"); + } + + //表名可以从被更新记录的Entity中获得,也可以从Where中获得 + String tableName = record.getTableName(); + if(StrUtil.isBlank(tableName)){ + tableName = where.getTableName(); + record.setTableName(tableName); + } + + final Query query = new Query(SqlUtil.buildConditions(where), tableName); + PreparedStatement ps = null; + try { + ps = dialect.psForUpdate(conn, record, query); + return ps.executeUpdate(); + } catch (SQLException e) { + throw e; + } finally { + DbUtil.close(ps); + } + } + + /** + * 查询
+ * 此方法不会关闭Connection + * + * @param 结果对象类型 + * @param conn 数据库连接对象 + * @param query {@link Query} + * @param rsh 结果集处理对象 + * @return 结果对象 + * @throws SQLException SQL执行异常 + */ + public T find(Connection conn, Query query, RsHandler rsh) throws SQLException { + checkConn(conn); + Assert.notNull(query, "[query] is null !"); + + PreparedStatement ps = null; + try { + ps = dialect.psForFind(conn, query); + return SqlExecutor.query(ps, rsh); + } catch (SQLException e) { + throw e; + } finally { + DbUtil.close(ps); + } + } + + /** + * 查询
+ * 此方法不会关闭Connection + * + * @param 结果对象类型 + * @param conn 数据库连接对象 + * @param fields 返回的字段列表,null则返回所有字段 + * @param where 条件实体类(包含表名) + * @param rsh 结果集处理对象 + * @return 结果对象 + * @throws SQLException SQL执行异常 + */ + public T find(Connection conn, Collection fields, Entity where, RsHandler rsh) throws SQLException { + final Query query = new Query(SqlUtil.buildConditions(where), where.getTableName()); + query.setFields(fields); + return find(conn, query, rsh); + } + + /** + * 查询,返回指定字段列表
+ * 此方法不会关闭Connection + * + * @param 结果对象类型 + * @param conn 数据库连接对象 + * @param where 条件实体类(包含表名) + * @param rsh 结果集处理对象 + * @param fields 字段列表,可变长参数如果无值表示查询全部字段 + * @return 结果对象 + * @throws SQLException SQL执行异常 + */ + public T find(Connection conn, Entity where, RsHandler rsh, String... fields) throws SQLException { + return find(conn, CollectionUtil.newArrayList(fields), where, rsh); + } + + /** + * 查询数据列表,返回字段在where参数中定义 + * + * @param conn 数据库连接对象 + * @param where 条件实体类(包含表名) + * @return 数据对象列表 + * @throws SQLException SQL执行异常 + * @since 3.2.1 + */ + public List find(Connection conn, Entity where) throws SQLException{ + return find(conn, where.getFieldNames(), where, EntityListHandler.create()); + } + + /** + * 查询数据列表,返回所有字段 + * + * @param conn 数据库连接对象 + * @param where 条件实体类(包含表名) + * @return 数据对象列表 + * @throws SQLException SQL执行异常 + */ + public List findAll(Connection conn, Entity where) throws SQLException{ + return find(conn, where, EntityListHandler.create()); + } + + /** + * 查询数据列表,返回所有字段 + * + * @param conn 数据库连接对象 + * @param tableName 表名 + * @return 数据对象列表 + * @throws SQLException SQL执行异常 + */ + public List findAll(Connection conn, String tableName) throws SQLException{ + return findAll(conn, Entity.create(tableName)); + } + + /** + * 根据某个字段名条件查询数据列表,返回所有字段 + * + * @param conn 数据库连接对象 + * @param tableName 表名 + * @param field 字段名 + * @param value 字段值 + * @return 数据对象列表 + * @throws SQLException SQL执行异常 + */ + public List findBy(Connection conn, String tableName, String field, Object value) throws SQLException{ + return findAll(conn, Entity.create(tableName).set(field, value)); + } + + /** + * 根据某个字段名条件查询数据列表,返回所有字段 + * + * @param conn 数据库连接对象 + * @param tableName 表名 + * @param field 字段名 + * @param value 字段值 + * @param likeType {@link LikeType} + * @return 数据对象列表 + * @throws SQLException SQL执行异常 + */ + public List findLike(Connection conn, String tableName, String field, String value, LikeType likeType) throws SQLException{ + return findAll(conn, Entity.create(tableName).set(field, SqlUtil.buildLikeValue(value, likeType, true))); + } + + /** + * 根据某个字段名条件查询数据列表,返回所有字段 + * + * @param conn 数据库连接对象 + * @param tableName 表名 + * @param field 字段名 + * @param values 字段值列表 + * @return 数据对象列表 + * @throws SQLException SQL执行异常 + */ + public List findIn(Connection conn, String tableName, String field, Object... values) throws SQLException{ + return findAll(conn, Entity.create(tableName).set(field, values)); + } + + /** + * 结果的条目数 + * @param conn 数据库连接对象 + * @param where 查询条件 + * @return 复合条件的结果数 + * @throws SQLException SQL执行异常 + */ + public int count(Connection conn, Entity where) throws SQLException { + checkConn(conn); + + final Query query = new Query(SqlUtil.buildConditions(where), where.getTableName()); + PreparedStatement ps = null; + try { + ps = dialect.psForCount(conn, query); + return SqlExecutor.query(ps, new NumberHandler()).intValue(); + } catch (SQLException e) { + throw e; + } finally { + DbUtil.close(ps); + } + } + + /** + * 分页查询
+ * 此方法不会关闭Connection + * + * @param 结果对象类型 + * @param conn 数据库连接对象 + * @param fields 返回的字段列表,null则返回所有字段 + * @param where 条件实体类(包含表名) + * @param pageNumber 页码 + * @param numPerPage 每页条目数 + * @param rsh 结果集处理对象 + * @return 结果对象 + * @throws SQLException SQL执行异常 + */ + public T page(Connection conn, Collection fields, Entity where, int pageNumber, int numPerPage, RsHandler rsh) throws SQLException { + return page(conn, fields, where, new Page(pageNumber, numPerPage), rsh); + } + + /** + * 分页查询
+ * 此方法不会关闭Connection + * + * @param 结果对象类型 + * @param conn 数据库连接对象 + * @param fields 返回的字段列表,null则返回所有字段 + * @param where 条件实体类(包含表名) + * @param page 分页对象 + * @param rsh 结果集处理对象 + * @return 结果对象 + * @throws SQLException SQL执行异常 + */ + public T page(Connection conn, Collection fields, Entity where, Page page, RsHandler rsh) throws SQLException { + checkConn(conn); + if(null == page){ + return this.find(conn, fields, where, rsh); + } + + final Query query = new Query(SqlUtil.buildConditions(where), where.getTableName()); + query.setFields(fields); + query.setPage(page); + return SqlExecutor.queryAndClosePs(dialect.psForPage(conn, query), rsh); + } + + /** + * 分页查询
+ * 此方法不会关闭Connection + * + * @param conn 数据库连接对象 + * @param fields 返回的字段列表,null则返回所有字段 + * @param where 条件实体类(包含表名) + * @param page 页码 + * @param numPerPage 每页条目数 + * @return 结果对象 + * @throws SQLException SQL执行异常 + */ + public PageResult page(Connection conn, Collection fields, Entity where, int page, int numPerPage) throws SQLException { + checkConn(conn); + + final int count = count(conn, where); + PageResultHandler pageResultHandler = PageResultHandler.create(new PageResult(page, numPerPage, count)); + return this.page(conn, fields, where, page, numPerPage, pageResultHandler); + } + + /** + * 分页查询
+ * 此方法不会关闭Connection + * + * @param conn 数据库连接对象 + * @param fields 返回的字段列表,null则返回所有字段 + * @param where 条件实体类(包含表名) + * @param page 分页对象 + * @return 结果对象 + * @throws SQLException SQL执行异常 + */ + public PageResult page(Connection conn, Collection fields, Entity where, Page page) throws SQLException { + checkConn(conn); + + //查询全部 + if(null == page){ + List entityList = this.find(conn, fields, where, new EntityListHandler()); + final PageResult pageResult = new PageResult(0, entityList.size(), entityList.size()); + pageResult.addAll(entityList); + return pageResult; + } + + final int count = count(conn, where); + PageResultHandler pageResultHandler = PageResultHandler.create(new PageResult(page.getPageNumber(), page.getPageSize(), count)); + return this.page(conn, fields, where, page, pageResultHandler); + } + + /** + * 分页全字段查询
+ * 此方法不会关闭Connection + * + * @param conn 数据库连接对象 + * @param where 条件实体类(包含表名) + * @param page 分页对象 + * @return 结果对象 + * @throws SQLException SQL执行异常 + */ + public PageResult page(Connection conn, Entity where, Page page) throws SQLException { + return this.page(conn, null, where, page); + } + //---------------------------------------------------------------------------- CRUD end + + //---------------------------------------------------------------------------- Getters and Setters end + /** + * @return SQL方言 + */ + public Dialect getDialect() { + return dialect; + } + /** + * 设置SQL方言 + * @param dialect 方言 + */ + public SqlConnRunner setDialect(Dialect dialect) { + this.dialect = dialect; + return this; + } + + /** + * 设置包装器,包装器用于对表名、字段名进行符号包装(例如双引号),防止关键字与这些表名或字段冲突 + * @param wrapperChar 包装字符,字符会在SQL生成时位于表名和字段名两边,null时表示取消包装 + * @return this + * @since 4.0.0 + */ + public SqlConnRunner setWrapper(Character wrapperChar) { + return setWrapper(new Wrapper(wrapperChar)); + } + + /** + * 设置包装器,包装器用于对表名、字段名进行符号包装(例如双引号),防止关键字与这些表名或字段冲突 + * @param wrapper 包装器,null表示取消包装 + * @return this + * @since 4.0.0 + */ + public SqlConnRunner setWrapper(Wrapper wrapper) { + this.dialect.setWrapper(wrapper); + return this; + } + //---------------------------------------------------------------------------- Getters and Setters end + + //---------------------------------------------------------------------------- Private method start + private void checkConn(Connection conn){ + if(null == conn){ + throw new NullPointerException("Connection object is null!"); + } + } + //---------------------------------------------------------------------------- Private method start +} \ No newline at end of file diff --git a/hutool-db/src/main/java/cn/hutool/db/SqlRunner.java b/hutool-db/src/main/java/cn/hutool/db/SqlRunner.java new file mode 100644 index 000000000..1f87b80a5 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/SqlRunner.java @@ -0,0 +1,130 @@ +package cn.hutool.db; + +import java.sql.Connection; +import java.sql.SQLException; + +import javax.sql.DataSource; + +import cn.hutool.db.dialect.Dialect; +import cn.hutool.db.dialect.DialectFactory; +import cn.hutool.db.ds.DSFactory; +import cn.hutool.db.sql.Wrapper; + +/** + * SQL执行类
+ * 通过给定的数据源执行给定SQL或者给定数据源和方言,执行相应的CRUD操作
+ * SqlRunner中每一个方法都会打开和关闭一个链接
+ * 此类为线程安全的对象,可以单例使用 + * + * @author Luxiaolei + * @deprecated 请使用{@link Db} + */ +@Deprecated +public class SqlRunner extends AbstractDb{ + private static final long serialVersionUID = 6626183393926198184L; + + /** + * 创建SqlRunner
+ * 使用默认数据源,自动探测数据库连接池 + * @return SqlRunner + * @since 3.0.6 + */ + public static SqlRunner create() { + return create(DSFactory.get()); + } + + /** + * 创建SqlRunner
+ * 使用默认数据源,自动探测数据库连接池 + * + * @param group 数据源分组 + * @return SqlRunner + * @since 4.0.11 + */ + public static SqlRunner create(String group) { + return create(DSFactory.get(group)); + } + + /** + * 创建SqlRunner
+ * 会根据数据源连接的元信息识别目标数据库类型,进而使用合适的数据源 + * @param ds 数据源 + * @return SqlRunner + */ + public static SqlRunner create(DataSource ds) { + return ds == null ? null : new SqlRunner(ds); + } + + /** + * 创建SqlRunner + * @param ds 数据源 + * @param dialect 方言 + * @return SqlRunner + */ + public static SqlRunner create(DataSource ds, Dialect dialect) { + return new SqlRunner(ds, dialect); + } + + /** + * 创建SqlRunner + * @param ds 数据源 + * @param driverClassName 数据库连接驱动类名 + * @return SqlRunner + */ + public static SqlRunner create(DataSource ds, String driverClassName) { + return new SqlRunner(ds, DialectFactory.newDialect(driverClassName)); + } + + //------------------------------------------------------- Constructor start + /** + * 构造,从DataSource中识别方言 + * @param ds 数据源 + */ + public SqlRunner(DataSource ds) { + this(ds, DialectFactory.getDialect(ds)); + } + + /** + * 构造 + * @param ds 数据源 + * @param driverClassName 数据库连接驱动类名,用于识别方言 + */ + public SqlRunner(DataSource ds, String driverClassName) { + this(ds, DialectFactory.newDialect(driverClassName)); + } + + /** + * 构造 + * @param ds 数据源 + * @param dialect 方言 + */ + public SqlRunner(DataSource ds, Dialect dialect) { + super(ds, dialect); + } + //------------------------------------------------------- Constructor end + + //---------------------------------------------------------------------------- Getters and Setters start + @Override + public SqlRunner setWrapper(Character wrapperChar) { + return (SqlRunner) super.setWrapper(wrapperChar); + } + + @Override + public SqlRunner setWrapper(Wrapper wrapper) { + return (SqlRunner) super.setWrapper(wrapper); + } + //---------------------------------------------------------------------------- Getters and Setters end + + @Override + public Connection getConnection() throws SQLException{ + return ds.getConnection(); + } + + @Override + public void closeConnection(Connection conn) { + DbUtil.close(conn); + } + + //---------------------------------------------------------------------------- Private method start + //---------------------------------------------------------------------------- Private method end +} \ No newline at end of file diff --git a/hutool-db/src/main/java/cn/hutool/db/StatementUtil.java b/hutool-db/src/main/java/cn/hutool/db/StatementUtil.java new file mode 100644 index 000000000..65938ad74 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/StatementUtil.java @@ -0,0 +1,261 @@ +package cn.hutool.db; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.ParameterMetaData; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Types; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import cn.hutool.core.collection.ArrayIter; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.db.sql.SqlBuilder; +import cn.hutool.db.sql.SqlLog; +import cn.hutool.db.sql.SqlUtil; + +/** + * Statement和PreparedStatement工具类 + * + * @author looly + * @since 4.0.10 + */ +public class StatementUtil { + /** + * 填充SQL的参数。 + * + * @param ps PreparedStatement + * @param params SQL参数 + * @return {@link PreparedStatement} + * @throws SQLException SQL执行异常 + */ + public static PreparedStatement fillParams(PreparedStatement ps, Collection params) throws SQLException { + return fillParams(ps, params.toArray(new Object[params.size()])); + } + + /** + * 填充SQL的参数。
+ * 对于日期对象特殊处理:传入java.util.Date默认按照Timestamp处理 + * + * @param ps PreparedStatement + * @param params SQL参数 + * @return {@link PreparedStatement} + * @throws SQLException SQL执行异常 + */ + public static PreparedStatement fillParams(PreparedStatement ps, Object... params) throws SQLException { + if (ArrayUtil.isEmpty(params)) { + return ps;// 无参数 + } + Object param; + for (int i = 0; i < params.length; i++) { + int paramIndex = i + 1; + param = params[i]; + if (null != param) { + if (param instanceof java.util.Date) { + // 日期特殊处理 + if (param instanceof java.sql.Date) { + ps.setDate(paramIndex, (java.sql.Date) param); + } else if (param instanceof java.sql.Time) { + ps.setTime(paramIndex, (java.sql.Time) param); + } else { + ps.setTimestamp(paramIndex, SqlUtil.toSqlTimestamp((java.util.Date) param)); + } + } else if (param instanceof Number) { + // 针对大数字类型的特殊处理 + if (param instanceof BigInteger) { + // BigInteger转为Long + ps.setLong(paramIndex, ((BigInteger) param).longValue()); + } else if (param instanceof BigDecimal) { + // BigDecimal的转换交给JDBC驱动处理 + ps.setBigDecimal(paramIndex, (BigDecimal) param); + } else { + // 普通数字类型按照默认传入 + ps.setObject(paramIndex, param); + } + } else { + ps.setObject(paramIndex, param); + } + } else { + final ParameterMetaData pmd = ps.getParameterMetaData(); + int sqlType = Types.VARCHAR; + try { + sqlType = pmd.getParameterType(paramIndex); + } catch (SQLException e) { + // ignore + // log.warn("Null param of index [{}] type get failed, by: {}", paramIndex, e.getMessage()); + } + ps.setNull(paramIndex, sqlType); + } + } + return ps; + } + + /** + * 创建{@link PreparedStatement} + * + * @param conn 数据库连接 + * @param sqlBuilder {@link SqlBuilder}包括SQL语句和参数 + * @return {@link PreparedStatement} + * @throws SQLException SQL异常 + * @since 4.1.3 + */ + public static PreparedStatement prepareStatement(Connection conn, SqlBuilder sqlBuilder) throws SQLException { + return prepareStatement(conn, sqlBuilder.build(), sqlBuilder.getParamValueArray()); + } + + /** + * 创建{@link PreparedStatement} + * + * @param conn 数据库连接 + * @param sql SQL语句,使用"?"做为占位符 + * @param params "?"对应参数列表 + * @return {@link PreparedStatement} + * @throws SQLException SQL异常 + * @since 3.2.3 + */ + public static PreparedStatement prepareStatement(Connection conn, String sql, Collection params) throws SQLException { + return prepareStatement(conn, sql, params.toArray(new Object[params.size()])); + } + + /** + * 创建{@link PreparedStatement} + * + * @param conn 数据库连接 + * @param sql SQL语句,使用"?"做为占位符 + * @param params "?"对应参数列表 + * @return {@link PreparedStatement} + * @throws SQLException SQL异常 + * @since 3.2.3 + */ + public static PreparedStatement prepareStatement(Connection conn, String sql, Object... params) throws SQLException { + Assert.notBlank(sql, "Sql String must be not blank!"); + + sql = sql.trim(); + SqlLog.INSTASNCE.log(sql, params); + PreparedStatement ps; + if (StrUtil.startWithIgnoreCase(sql, "insert")) { + // 插入默认返回主键 + ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); + } else { + ps = conn.prepareStatement(sql); + } + return fillParams(ps, params); + } + + /** + * 创建批量操作的{@link PreparedStatement} + * + * @param conn 数据库连接 + * @param sql SQL语句,使用"?"做为占位符 + * @param paramsBatch "?"对应参数批次列表 + * @return {@link PreparedStatement} + * @throws SQLException SQL异常 + * @since 4.1.13 + */ + public static PreparedStatement prepareStatementForBatch(Connection conn, String sql, Object[]... paramsBatch) throws SQLException { + return prepareStatementForBatch(conn, sql, new ArrayIter(paramsBatch)); + } + + /** + * 创建批量操作的{@link PreparedStatement} + * + * @param conn 数据库连接 + * @param sql SQL语句,使用"?"做为占位符 + * @param paramsBatch "?"对应参数批次列表 + * @return {@link PreparedStatement} + * @throws SQLException SQL异常 + * @since 4.1.13 + */ + public static PreparedStatement prepareStatementForBatch(Connection conn, String sql, Iterable paramsBatch) throws SQLException { + Assert.notBlank(sql, "Sql String must be not blank!"); + + sql = sql.trim(); + SqlLog.INSTASNCE.log(sql, paramsBatch); + PreparedStatement ps = conn.prepareStatement(sql); + for (Object[] params : paramsBatch) { + StatementUtil.fillParams(ps, params); + ps.addBatch(); + } + return ps; + } + + /** + * 创建{@link CallableStatement} + * + * @param conn 数据库连接 + * @param sql SQL语句,使用"?"做为占位符 + * @param params "?"对应参数列表 + * @return {@link CallableStatement} + * @throws SQLException SQL异常 + * @since 4.1.13 + */ + public static CallableStatement prepareCall(Connection conn, String sql, Object... params) throws SQLException { + Assert.notBlank(sql, "Sql String must be not blank!"); + + sql = sql.trim(); + SqlLog.INSTASNCE.log(sql, params); + final CallableStatement call = conn.prepareCall(sql); + fillParams(call, params); + return call; + } + + /** + * 获得自增键的值
+ * 此方法对于Oracle无效 + * + * @param ps PreparedStatement + * @return 自增键的值 + * @throws SQLException SQL执行异常 + */ + public static Long getGeneratedKeyOfLong(PreparedStatement ps) throws SQLException { + ResultSet rs = null; + try { + rs = ps.getGeneratedKeys(); + Long generatedKey = null; + if (rs != null && rs.next()) { + try { + generatedKey = rs.getLong(1); + } catch (SQLException e) { + // 自增主键不为数字或者为Oracle的rowid,跳过 + } + } + return generatedKey; + } catch (SQLException e) { + throw e; + } finally { + DbUtil.close(rs); + } + } + + /** + * 获得所有主键
+ * + * @param ps PreparedStatement + * @return 所有主键 + * @throws SQLException SQL执行异常 + */ + public static List getGeneratedKeys(PreparedStatement ps) throws SQLException { + List keys = new ArrayList(); + ResultSet rs = null; + int i = 1; + try { + rs = ps.getGeneratedKeys(); + if (rs != null && rs.next()) { + keys.add(rs.getObject(i++)); + } + return keys; + } catch (SQLException e) { + throw e; + } finally { + DbUtil.close(rs); + } + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/ThreadLocalConnection.java b/hutool-db/src/main/java/cn/hutool/db/ThreadLocalConnection.java new file mode 100644 index 000000000..19f7cab08 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/ThreadLocalConnection.java @@ -0,0 +1,101 @@ +package cn.hutool.db; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; + +import javax.sql.DataSource; + +/** + * 线程相关的数据库连接持有器
+ * 此对象为单例类,用于存储线程相关的Connection对象。
+ * 在多数据源情况下,由于数据源的不同,连接对象也不同,因此获取连接时需要DataSource关联获取 + * + * @author looly + * + */ +public enum ThreadLocalConnection { + INSTANCE; + + private final ThreadLocal threadLocal = new ThreadLocal<>(); + + /** + * 获取数据源对应的数据库连接 + * + * @param ds 数据源 + * @return Connection + * @throws SQLException SQL异常 + */ + public Connection get(DataSource ds) throws SQLException { + GroupedConnection groupedConnection = threadLocal.get(); + if (null == groupedConnection) { + groupedConnection = new GroupedConnection(); + threadLocal.set(groupedConnection); + } + return groupedConnection.get(ds); + } + + /** + * 关闭数据库,并从线程池中移除 + * + * @param ds 数据源 + * @since 4.1.7 + */ + public void close(DataSource ds) { + GroupedConnection groupedConnection = threadLocal.get(); + if (null != groupedConnection) { + groupedConnection.close(ds); + threadLocal.remove(); + } + } + + /** + * 分组连接,根据不同的分组获取对应的连接,用于多数据源情况 + */ + public static class GroupedConnection { + + /** 连接的Map,考虑到大部分情况是单数据库,故此处初始大小1 */ + private Map connMap = new HashMap<>(1, 1); + + /** + * 获取连接,如果获取的连接为空或者已被关闭,重新创建连接 + * + * @param ds 数据源 + * @return Connection + * @throws SQLException + */ + public Connection get(DataSource ds) throws SQLException { + Connection conn = connMap.get(ds); + if (null == conn || conn.isClosed()) { + conn = ds.getConnection(); + connMap.put(ds, conn); + } + return conn; + } + + /** + * 关闭并移除Connection
+ * 如果处于事务中,则不进行任何操作 + * + * @param ds 数据源 + * @return this + */ + public GroupedConnection close(DataSource ds) { + final Connection conn = connMap.get(ds); + if(null != conn) { + try { + if(false == conn.getAutoCommit()) { + //非自动提交事务的连接,不做关闭(可能处于事务中) + return this; + } + } catch (SQLException e) { + //ignore + } + connMap.remove(ds); + DbUtil.close(conn); + } + return this; + } + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/dialect/Dialect.java b/hutool-db/src/main/java/cn/hutool/db/dialect/Dialect.java new file mode 100644 index 000000000..6bca51289 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/dialect/Dialect.java @@ -0,0 +1,113 @@ +package cn.hutool.db.dialect; + +import java.io.Serializable; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import cn.hutool.db.Entity; +import cn.hutool.db.sql.Query; +import cn.hutool.db.sql.Wrapper; + +/** + * SQL方言,不同的数据库由于在某些SQL上有所区别,故为每种数据库配置不同的方言。
+ * 由于不同数据库间SQL语句的差异,导致无法统一拼接SQL,
+ * Dialect接口旨在根据不同的数据库,使用不同的方言实现类,来拼接对应的SQL,并将SQL和参数放入PreparedStatement中 + * + * @author loolly + * + */ +public interface Dialect extends Serializable{ + + /** + * @return 包装器 + */ + Wrapper getWrapper(); + + /** + * 设置包装器 + * + * @param wrapper 包装器 + */ + void setWrapper(Wrapper wrapper); + + // -------------------------------------------- Execute + /** + * 构建用于插入的PreparedStatement + * + * @param conn 数据库连接对象 + * @param entity 数据实体类(包含表名) + * @return PreparedStatement + * @throws SQLException SQL执行异常 + */ + PreparedStatement psForInsert(Connection conn, Entity entity) throws SQLException; + + /** + * 构建用于批量插入的PreparedStatement + * + * @param conn 数据库连接对象 + * @param entities 数据实体,实体的结构必须全部一致,否则插入结果将不可预知 + * @return PreparedStatement + * @throws SQLException SQL执行异常 + */ + PreparedStatement psForInsertBatch(Connection conn, Entity... entities) throws SQLException; + + /** + * 构建用于删除的PreparedStatement + * + * @param conn 数据库连接对象 + * @param query 查找条件(包含表名) + * @return PreparedStatement + * @throws SQLException SQL执行异常 + */ + PreparedStatement psForDelete(Connection conn, Query query) throws SQLException; + + /** + * 构建用于更新的PreparedStatement + * + * @param conn 数据库连接对象 + * @param entity 数据实体类(包含表名) + * @param query 查找条件(包含表名) + * @return PreparedStatement + * @throws SQLException SQL执行异常 + */ + PreparedStatement psForUpdate(Connection conn, Entity entity, Query query) throws SQLException; + + // -------------------------------------------- Query + /** + * 构建用于获取多条记录的PreparedStatement + * + * @param conn 数据库连接对象 + * @param query 查询条件(包含表名) + * @return PreparedStatement + * @throws SQLException SQL执行异常 + */ + PreparedStatement psForFind(Connection conn, Query query) throws SQLException; + + /** + * 构建用于分页查询的PreparedStatement + * + * @param conn 数据库连接对象 + * @param query 查询条件(包含表名) + * @return PreparedStatement + * @throws SQLException SQL执行异常 + */ + PreparedStatement psForPage(Connection conn, Query query) throws SQLException; + + /** + * 构建用于查询行数的PreparedStatement + * + * @param conn 数据库连接对象 + * @param query 查询条件(包含表名) + * @return PreparedStatement + * @throws SQLException SQL执行异常 + */ + PreparedStatement psForCount(Connection conn, Query query) throws SQLException; + + /** + * 方言名 + * + * @return 方言名 + */ + DialectName dialectName(); +} diff --git a/hutool-db/src/main/java/cn/hutool/db/dialect/DialectFactory.java b/hutool-db/src/main/java/cn/hutool/db/dialect/DialectFactory.java new file mode 100644 index 000000000..1698a4c4e --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/dialect/DialectFactory.java @@ -0,0 +1,187 @@ +package cn.hutool.db.dialect; + +import java.sql.Connection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.sql.DataSource; + +import cn.hutool.core.util.ClassLoaderUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.db.dialect.impl.AnsiSqlDialect; +import cn.hutool.db.dialect.impl.H2Dialect; +import cn.hutool.db.dialect.impl.MysqlDialect; +import cn.hutool.db.dialect.impl.OracleDialect; +import cn.hutool.db.dialect.impl.PostgresqlDialect; +import cn.hutool.db.dialect.impl.SqlServer2012Dialect; +import cn.hutool.db.dialect.impl.Sqlite3Dialect; +import cn.hutool.log.StaticLog; + +/** + * 方言工厂类 + * + * @author loolly + * + */ +public class DialectFactory { + + /** JDBC 驱动 MySQL */ + public final static String DRIVER_MYSQL = "com.mysql.jdbc.Driver"; + /** JDBC 驱动 MySQL,在6.X版本中变动驱动类名,且使用SPI机制 */ + public final static String DRIVER_MYSQL_V6 = "com.mysql.cj.jdbc.Driver"; + /** JDBC 驱动 Oracle */ + public final static String DRIVER_ORACLE = "oracle.jdbc.OracleDriver"; + /** JDBC 驱动 Oracle,旧版使用 */ + public final static String DRIVER_ORACLE_OLD = "oracle.jdbc.driver.OracleDriver"; + /** JDBC 驱动 PostgreSQL */ + public final static String DRIVER_POSTGRESQL = "org.postgresql.Driver"; + /** JDBC 驱动 SQLLite3 */ + public final static String DRIVER_SQLLITE3 = "org.sqlite.JDBC"; + /** JDBC 驱动 SQLServer */ + public final static String DRIVER_SQLSERVER = "com.microsoft.sqlserver.jdbc.SQLServerDriver"; + /** JDBC 驱动 Hive */ + public final static String DRIVER_HIVE = "org.apache.hadoop.hive.jdbc.HiveDriver"; + /** JDBC 驱动 Hive2 */ + public final static String DRIVER_HIVE2 = "org.apache.hive.jdbc.HiveDriver"; + /** JDBC 驱动 H2 */ + public final static String DRIVER_H2 = "org.h2.Driver"; + /** JDBC 驱动 Derby */ + public final static String DRIVER_DERBY = "org.apache.derby.jdbc.ClientDriver"; + /** JDBC 驱动 Derby嵌入式 */ + public final static String DRIVER_DERBY_EMBEDDED = "org.apache.derby.jdbc.EmbeddedDriver"; + /** JDBC 驱动 HSQLDB */ + public final static String DRIVER_HSQLDB = "org.hsqldb.jdbc.JDBCDriver"; + /** JDBC 驱动 达梦7 */ + public final static String DRIVER_DM7 = "dm.jdbc.driver.DmDriver"; + + private static Map dialectPool = new ConcurrentHashMap<>(); + private static Object lock = new Object(); + + private DialectFactory() { + } + + /** + * 根据驱动名创建方言
+ * 驱动名是不分区大小写完全匹配的 + * + * @param driverName JDBC驱动类名 + * @return 方言 + */ + public static Dialect newDialect(String driverName) { + final Dialect dialect = internalNewDialect(driverName); + StaticLog.debug("Use Dialect: [{}].", dialect.getClass().getSimpleName()); + return dialect; + } + + /** + * 根据驱动名创建方言
+ * 驱动名是不分区大小写完全匹配的 + * + * @param driverName JDBC驱动类名 + * @return 方言 + */ + private static Dialect internalNewDialect(String driverName) { + if (StrUtil.isNotBlank(driverName)) { + if (DRIVER_MYSQL.equalsIgnoreCase(driverName) || DRIVER_MYSQL_V6.equalsIgnoreCase(driverName)) { + return new MysqlDialect(); + } else if (DRIVER_ORACLE.equalsIgnoreCase(driverName) || DRIVER_ORACLE_OLD.equalsIgnoreCase(driverName)) { + return new OracleDialect(); + } else if (DRIVER_SQLLITE3.equalsIgnoreCase(driverName)) { + return new Sqlite3Dialect(); + } else if (DRIVER_POSTGRESQL.equalsIgnoreCase(driverName)) { + return new PostgresqlDialect(); + } else if (DRIVER_H2.equalsIgnoreCase(driverName)) { + return new H2Dialect(); + } else if (DRIVER_SQLSERVER.equalsIgnoreCase(driverName)) { + return new SqlServer2012Dialect(); + } + } + // 无法识别可支持的数据库类型默认使用ANSI方言,可兼容大部分SQL语句 + return new AnsiSqlDialect(); + } + + /** + * 通过JDBC URL等信息识别JDBC驱动名 + * + * @param nameContainsProductInfo 包含数据库标识的字符串 + * @return 驱动 + */ + public static String identifyDriver(String nameContainsProductInfo) { + if (StrUtil.isBlank(nameContainsProductInfo)) { + return null; + } + // 全部转为小写,忽略大小写 + nameContainsProductInfo = StrUtil.cleanBlank(nameContainsProductInfo.toLowerCase()); + + String driver = null; + if (nameContainsProductInfo.contains("mysql")) { + driver = ClassLoaderUtil.isPresent(DRIVER_MYSQL_V6) ? DRIVER_MYSQL_V6 : DRIVER_MYSQL; + } else if (nameContainsProductInfo.contains("oracle")) { + driver = ClassLoaderUtil.isPresent(DRIVER_ORACLE) ? DRIVER_ORACLE : DRIVER_ORACLE_OLD; + } else if (nameContainsProductInfo.contains("postgresql")) { + driver = DRIVER_POSTGRESQL; + } else if (nameContainsProductInfo.contains("sqlite")) { + driver = DRIVER_SQLLITE3; + } else if (nameContainsProductInfo.contains("sqlserver")) { + driver = DRIVER_SQLSERVER; + } else if (nameContainsProductInfo.contains("hive")) { + driver = DRIVER_HIVE; + } else if (nameContainsProductInfo.contains("h2")) { + driver = DRIVER_H2; + } else if (nameContainsProductInfo.startsWith("jdbc:derby://")) { + // Derby数据库网络连接方式 + driver = DRIVER_DERBY; + } else if (nameContainsProductInfo.contains("derby")) { + // 嵌入式Derby数据库 + driver = DRIVER_DERBY_EMBEDDED; + } else if (nameContainsProductInfo.contains("hsqldb")) { + // HSQLDB + driver = DRIVER_HSQLDB; + } else if (nameContainsProductInfo.contains("dm")) { + // 达梦7 + driver = DRIVER_DM7; + } + + return driver; + } + + /** + * 获取共享方言 + * @param ds 数据源,每一个数据源对应一个唯一方言 + * @return {@link Dialect}方言 + */ + public static Dialect getDialect(DataSource ds) { + Dialect dialect = dialectPool.get(ds); + if(null == dialect) { + synchronized (lock) { + dialect = dialectPool.get(ds); + if(null == dialect) { + dialect = newDialect(ds); + dialectPool.put(ds, dialect); + } + } + } + return dialect; + } + + /** + * 创建方言 + * + * @param ds 数据源 + * @return 方言 + */ + public static Dialect newDialect(DataSource ds) { + return newDialect(DriverUtil.identifyDriver(ds)); + } + + /** + * 创建方言 + * + * @param conn 数据库连接对象 + * @return 方言 + */ + public static Dialect newDialect(Connection conn) { + return newDialect(DriverUtil.identifyDriver(conn)); + } + +} diff --git a/hutool-db/src/main/java/cn/hutool/db/dialect/DialectName.java b/hutool-db/src/main/java/cn/hutool/db/dialect/DialectName.java new file mode 100644 index 000000000..63ecd1396 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/dialect/DialectName.java @@ -0,0 +1,10 @@ +package cn.hutool.db.dialect; + +/** + * 方言名 + * @author Looly + * + */ +public enum DialectName { + ANSI, MYSQL, ORACLE, POSTGREESQL, SQLITE3, H2, SQLSERVER, SQLSERVER2012 +} diff --git a/hutool-db/src/main/java/cn/hutool/db/dialect/DriverUtil.java b/hutool-db/src/main/java/cn/hutool/db/dialect/DriverUtil.java new file mode 100644 index 000000000..ff25afd93 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/dialect/DriverUtil.java @@ -0,0 +1,86 @@ +package cn.hutool.db.dialect; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + +import javax.sql.DataSource; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.db.DbRuntimeException; +import cn.hutool.db.DbUtil; +import cn.hutool.db.ds.DataSourceWrapper; + +/** + * 驱动相关工具类,包括自动获取驱动类名 + * + * @author looly + * @since 4.0.10 + */ +public class DriverUtil { + /** + * 通过JDBC URL等信息识别JDBC驱动名 + * + * @param nameContainsProductInfo 包含数据库标识的字符串 + * @return 驱动 + * @see DialectFactory#identifyDriver(String) + */ + public static String identifyDriver(String nameContainsProductInfo) { + return DialectFactory.identifyDriver(nameContainsProductInfo); + } + + /** + * 识别JDBC驱动名 + * + * @param ds 数据源 + * @return 驱动 + */ + public static String identifyDriver(DataSource ds) { + if(ds instanceof DataSourceWrapper) { + final String driver = ((DataSourceWrapper)ds).getDriver(); + if(StrUtil.isNotBlank(driver)) { + return driver; + } + } + + Connection conn = null; + String driver = null; + try { + try { + conn = ds.getConnection(); + } catch (SQLException e) { + throw new DbRuntimeException("Get Connection error !", e); + } catch (NullPointerException e) { + throw new DbRuntimeException("Unexpected NullPointException, maybe [jdbcUrl] or [url] is empty!", e); + } + driver = identifyDriver(conn); + } finally { + DbUtil.close(conn); + } + + return driver; + } + + /** + * 识别JDBC驱动名 + * + * @param conn 数据库连接对象 + * @return 驱动 + * @throws DbRuntimeException SQL异常包装,获取元数据信息失败 + */ + public static String identifyDriver(Connection conn) throws DbRuntimeException { + String driver = null; + DatabaseMetaData meta; + try { + meta = conn.getMetaData(); + driver = identifyDriver(meta.getDatabaseProductName()); + if (StrUtil.isBlank(driver)) { + driver = identifyDriver(meta.getDriverName()); + } + } catch (SQLException e) { + throw new DbRuntimeException("Identify driver error!", e); + } + + return driver; + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/AnsiSqlDialect.java b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/AnsiSqlDialect.java new file mode 100644 index 000000000..49eecec70 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/AnsiSqlDialect.java @@ -0,0 +1,153 @@ +package cn.hutool.db.dialect.impl; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.db.DbRuntimeException; +import cn.hutool.db.Entity; +import cn.hutool.db.Page; +import cn.hutool.db.StatementUtil; +import cn.hutool.db.dialect.Dialect; +import cn.hutool.db.dialect.DialectName; +import cn.hutool.db.sql.Condition; +import cn.hutool.db.sql.LogicalOperator; +import cn.hutool.db.sql.Query; +import cn.hutool.db.sql.SqlBuilder; +import cn.hutool.db.sql.Wrapper; + +/** + * ANSI SQL 方言 + * + * @author loolly + * + */ +public class AnsiSqlDialect implements Dialect { + private static final long serialVersionUID = 2088101129774974580L; + + protected Wrapper wrapper = new Wrapper(); + + @Override + public Wrapper getWrapper() { + return this.wrapper; + } + + @Override + public void setWrapper(Wrapper wrapper) { + this.wrapper = wrapper; + } + + @Override + public PreparedStatement psForInsert(Connection conn, Entity entity) throws SQLException { + final SqlBuilder insert = SqlBuilder.create(wrapper).insert(entity, this.dialectName()); + + return StatementUtil.prepareStatement(conn, insert); + } + + @Override + public PreparedStatement psForInsertBatch(Connection conn, Entity... entities) throws SQLException { + if (ArrayUtil.isEmpty(entities)) { + throw new DbRuntimeException("Entities for batch insert is empty !"); + } + // 批量 + final SqlBuilder insert = SqlBuilder.create(wrapper).insert(entities[0], this.dialectName()); + + final PreparedStatement ps = StatementUtil.prepareStatement(conn, insert.build()); + for (Entity entity : entities) { + StatementUtil.fillParams(ps, CollectionUtil.valuesOfKeys(entity, insert.getFields())); + ps.addBatch(); + } + return ps; + } + + @Override + public PreparedStatement psForDelete(Connection conn, Query query) throws SQLException { + Assert.notNull(query, "query must not be null !"); + + final Condition[] where = query.getWhere(); + if (ArrayUtil.isEmpty(where)) { + // 对于无条件的删除语句直接抛出异常禁止,防止误删除 + throw new SQLException("No 'WHERE' condition, we can't prepared statement for delete everything."); + } + final SqlBuilder delete = SqlBuilder.create(wrapper).delete(query.getFirstTableName()).where(LogicalOperator.AND, where); + + return StatementUtil.prepareStatement(conn, delete); + } + + @Override + public PreparedStatement psForUpdate(Connection conn, Entity entity, Query query) throws SQLException { + Assert.notNull(query, "query must not be null !"); + + Condition[] where = query.getWhere(); + if (ArrayUtil.isEmpty(where)) { + // 对于无条件的删除语句直接抛出异常禁止,防止误删除 + throw new SQLException("No 'WHERE' condition, we can't prepare statement for update everything."); + } + + final SqlBuilder update = SqlBuilder.create(wrapper).update(entity).where(LogicalOperator.AND, where); + + return StatementUtil.prepareStatement(conn, update); + } + + @Override + public PreparedStatement psForFind(Connection conn, Query query) throws SQLException { + Assert.notNull(query, "query must not be null !"); + + final SqlBuilder find = SqlBuilder.create(wrapper).query(query); + + return StatementUtil.prepareStatement(conn, find); + } + + @Override + public PreparedStatement psForPage(Connection conn, Query query) throws SQLException { + // 验证 + if (query == null || StrUtil.hasBlank(query.getTableNames())) { + throw new DbRuntimeException("Table name must not be null !"); + } + + final Page page = query.getPage(); + if (null == page) { + // 无分页信息默认使用find + return this.psForFind(conn, query); + } + + SqlBuilder find = SqlBuilder.create(wrapper).query(query).orderBy(page.getOrders()); + + // 根据不同数据库在查询SQL语句基础上包装其分页的语句 + find = wrapPageSql(find, page); + + return StatementUtil.prepareStatement(conn, find); + } + + /** + * 根据不同数据库在查询SQL语句基础上包装其分页的语句
+ * 各自数据库通过重写此方法实现最小改动情况下修改分页语句 + * + * @param find 标准查询语句 + * @param page 分页对象 + * @return 分页语句 + * @since 3.2.3 + */ + protected SqlBuilder wrapPageSql(SqlBuilder find, Page page) { + // limit A offset B 表示:A就是你需要多少行,B就是查询的起点位置。 + return find.append(" limit ").append(page.getPageSize()).append(" offset ").append(page.getStartPosition()); + } + + @Override + public PreparedStatement psForCount(Connection conn, Query query) throws SQLException { + query.setFields(CollectionUtil.newArrayList("count(1)")); + return psForFind(conn, query); + } + + @Override + public DialectName dialectName() { + return DialectName.ANSI; + } + + // ---------------------------------------------------------------------------- Protected method start + // ---------------------------------------------------------------------------- Protected method end +} diff --git a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/H2Dialect.java b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/H2Dialect.java new file mode 100644 index 000000000..0d86f061a --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/H2Dialect.java @@ -0,0 +1,31 @@ +package cn.hutool.db.dialect.impl; + +import cn.hutool.db.Page; +import cn.hutool.db.dialect.DialectName; +import cn.hutool.db.sql.SqlBuilder; +import cn.hutool.db.sql.Wrapper; + +/** + * H2数据库方言 + * + * @author loolly + * + */ +public class H2Dialect extends AnsiSqlDialect { + private static final long serialVersionUID = 1490520247974768214L; + + public H2Dialect() { + wrapper = new Wrapper('"', '"'); + } + + @Override + public DialectName dialectName() { + return DialectName.H2; + } + + @Override + protected SqlBuilder wrapPageSql(SqlBuilder find, Page page) { + // limit A , B 表示:A就是查询的起点位置,B就是你需要多少行。 + return find.append(" limit ").append(page.getStartPosition()).append(" , ").append(page.getPageSize()); + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/MysqlDialect.java b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/MysqlDialect.java new file mode 100644 index 000000000..528806ad8 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/MysqlDialect.java @@ -0,0 +1,29 @@ +package cn.hutool.db.dialect.impl; + +import cn.hutool.db.Page; +import cn.hutool.db.dialect.DialectName; +import cn.hutool.db.sql.SqlBuilder; +import cn.hutool.db.sql.Wrapper; + +/** + * MySQL方言 + * @author loolly + * + */ +public class MysqlDialect extends AnsiSqlDialect{ + private static final long serialVersionUID = -3734718212043823636L; + + public MysqlDialect() { + wrapper = new Wrapper('`'); + } + + @Override + protected SqlBuilder wrapPageSql(SqlBuilder find, Page page) { + return find.append(" LIMIT ").append(page.getStartPosition()).append(", ").append(page.getPageSize()); + } + + @Override + public DialectName dialectName() { + return DialectName.MYSQL; + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/OracleDialect.java b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/OracleDialect.java new file mode 100644 index 000000000..d89c66e88 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/OracleDialect.java @@ -0,0 +1,33 @@ +package cn.hutool.db.dialect.impl; + +import cn.hutool.db.Page; +import cn.hutool.db.dialect.DialectName; +import cn.hutool.db.sql.SqlBuilder; + +/** + * Oracle 方言 + * @author loolly + * + */ +public class OracleDialect extends AnsiSqlDialect{ + private static final long serialVersionUID = 6122761762247483015L; + + public OracleDialect() { +// wrapper = new Wrapper('"'); //Oracle所有字段名用双引号包围,防止字段名或表名与系统关键字冲突 + } + + @Override + protected SqlBuilder wrapPageSql(SqlBuilder find, Page page) { + final int[] startEnd = page.getStartEnd(); + return find + .insertPreFragment("SELECT * FROM ( SELECT row_.*, rownum rownum_ from ( ") + .append(" ) row_ where rownum <= ").append(startEnd[1])// + .append(") table_alias")// + .append(" where table_alias.rownum_ > ").append(startEnd[0]);// + } + + @Override + public DialectName dialectName() { + return DialectName.ORACLE; + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/PostgresqlDialect.java b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/PostgresqlDialect.java new file mode 100644 index 000000000..404130fea --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/PostgresqlDialect.java @@ -0,0 +1,23 @@ +package cn.hutool.db.dialect.impl; + +import cn.hutool.db.dialect.DialectName; +import cn.hutool.db.sql.Wrapper; + + +/** + * Postgree方言 + * @author loolly + * + */ +public class PostgresqlDialect extends AnsiSqlDialect{ + private static final long serialVersionUID = 3889210427543389642L; + + public PostgresqlDialect() { + wrapper = new Wrapper('"'); + } + + @Override + public DialectName dialectName() { + return DialectName.POSTGREESQL; + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/SqlServer2012Dialect.java b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/SqlServer2012Dialect.java new file mode 100644 index 000000000..bc8e38588 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/SqlServer2012Dialect.java @@ -0,0 +1,40 @@ +package cn.hutool.db.dialect.impl; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.db.Page; +import cn.hutool.db.dialect.DialectName; +import cn.hutool.db.sql.SqlBuilder; +import cn.hutool.db.sql.Wrapper; + +/** + * SQLServer2012 方言 + * + * @author loolly + * + */ +public class SqlServer2012Dialect extends AnsiSqlDialect { + private static final long serialVersionUID = -37598166015777797L; + + public SqlServer2012Dialect() { + //双引号和中括号适用,双引号更广泛 + wrapper = new Wrapper('"'); + } + + @Override + protected SqlBuilder wrapPageSql(SqlBuilder find, Page page) { + if (false == StrUtil.containsIgnoreCase(find.toString(), "order by")) { + //offset 分页必须要跟在order by后面,没有情况下补充默认排序 + find.append(" order by current_timestamp"); + } + return find.append(" offset ") + .append(page.getStartPosition())// + .append(" row fetch next ")//row和rows同义词 + .append(page.getPageSize())// + .append(" row only");// + } + + @Override + public DialectName dialectName() { + return DialectName.SQLSERVER2012; + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/Sqlite3Dialect.java b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/Sqlite3Dialect.java new file mode 100644 index 000000000..1531b9fee --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/Sqlite3Dialect.java @@ -0,0 +1,22 @@ +package cn.hutool.db.dialect.impl; + +import cn.hutool.db.dialect.DialectName; +import cn.hutool.db.sql.Wrapper; + +/** + * SqlLite3方言 + * @author loolly + * + */ +public class Sqlite3Dialect extends AnsiSqlDialect{ + private static final long serialVersionUID = -3527642408849291634L; + + public Sqlite3Dialect() { + wrapper = new Wrapper('[', ']'); + } + + @Override + public DialectName dialectName() { + return DialectName.SQLITE3; + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/package-info.java b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/package-info.java new file mode 100644 index 000000000..2e852a7cb --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/package-info.java @@ -0,0 +1,7 @@ +/** + * 数据库方言实现,包括MySQL、Oracle、PostgreSQL、Sqlite3、H2、SqlServer2012等 + * + * @author looly + * + */ +package cn.hutool.db.dialect.impl; \ No newline at end of file diff --git a/hutool-db/src/main/java/cn/hutool/db/dialect/package-info.java b/hutool-db/src/main/java/cn/hutool/db/dialect/package-info.java new file mode 100644 index 000000000..bdd9b3f48 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/dialect/package-info.java @@ -0,0 +1,7 @@ +/** + * 数据库方言封装,包括数据库方言以及方言自动识别等 + * + * @author looly + * + */ +package cn.hutool.db.dialect; \ No newline at end of file diff --git a/hutool-db/src/main/java/cn/hutool/db/ds/AbstractDSFactory.java b/hutool-db/src/main/java/cn/hutool/db/ds/AbstractDSFactory.java new file mode 100644 index 000000000..c0c8cb199 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/ds/AbstractDSFactory.java @@ -0,0 +1,204 @@ +package cn.hutool.db.ds; + +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.sql.DataSource; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.io.resource.NoResourceException; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.hutool.db.DbRuntimeException; +import cn.hutool.db.DbUtil; +import cn.hutool.db.dialect.DriverUtil; +import cn.hutool.setting.Setting; + +/** + * 抽象数据源工厂
+ * 此工厂抽象类用于实现数据源的缓存,当用户多次调用{@link #getDataSource(String)} 时,工厂只需创建一次即可。
+ * 数据源是与配置文件中的分组相关的,每个分组的数据源相互独立,也就是每个分组的数据源是单例存在的。 + * + * @author looly + * + */ +public abstract class AbstractDSFactory extends DSFactory { + private static final long serialVersionUID = -6407302276272379881L; + + /** 数据库配置文件可选路径1 */ + private static final String DEFAULT_DB_SETTING_PATH = "config/db.setting"; + /** 数据库配置文件可选路径2 */ + private static final String DEFAULT_DB_SETTING_PATH2 = "db.setting"; + + /** 数据库连接配置文件 */ + private Setting setting; + /** 数据源池 */ + private Map dsMap; + + /** + * 构造 + * + * @param dataSourceName 数据源名称 + * @param dataSourceClass 数据库连接池实现类,用于检测所提供的DataSource类是否存在,当传入的DataSource类不存在时抛出ClassNotFoundException
+ * 此参数的作用是在detectDSFactory方法自动检测所用连接池时,如果实现类不存在,调用此方法会自动抛出异常,从而切换到下一种连接池的检测。 + * @param setting 数据库连接配置 + */ + public AbstractDSFactory(String dataSourceName, Class dataSourceClass, Setting setting) { + super(dataSourceName); + //此参数的作用是在detectDSFactory方法自动检测所用连接池时,如果实现类不存在,调用此方法会自动抛出异常,从而切换到下一种连接池的检测。 + Assert.notNull(dataSourceClass); + if (null == setting) { + try { + setting = new Setting(DEFAULT_DB_SETTING_PATH, true); + } catch (NoResourceException e) { + // 尝试ClassPath下直接读取配置文件 + try { + setting = new Setting(DEFAULT_DB_SETTING_PATH2, true); + } catch (NoResourceException e2) { + throw new NoResourceException("Default db setting [{}] or [{}] in classpath not found !", DEFAULT_DB_SETTING_PATH, DEFAULT_DB_SETTING_PATH2); + } + } + } + + // 读取配置,用于SQL打印 + DbUtil.setShowSqlGlobal(setting); + + this.setting = setting; + this.dsMap = new ConcurrentHashMap<>(); + } + + /** + * 获取配置,用于自定义添加配置项 + * + * @return Setting + * @since 4.0.3 + */ + public Setting getSetting() { + return this.setting; + } + + @Override + synchronized public DataSource getDataSource(String group) { + if (group == null) { + group = StrUtil.EMPTY; + } + + // 如果已经存在已有数据源(连接池)直接返回 + final DataSourceWrapper existedDataSource = dsMap.get(group); + if (existedDataSource != null) { + return existedDataSource; + } + + final DataSourceWrapper ds = createDataSource(group); + // 添加到数据源池中,以备下次使用 + dsMap.put(group, ds); + return ds; + } + + /** + * 创建数据源 + * + * @param group 分组 + * @return {@link DataSourceWrapper} 数据源包装 + */ + private DataSourceWrapper createDataSource(String group) { + if (group == null) { + group = StrUtil.EMPTY; + } + + final Setting config = setting.getSetting(group); + if (CollectionUtil.isEmpty(config)) { + throw new DbRuntimeException("No config for group: [{}]", group); + } + + // 基本信息 + final String url = config.getAndRemoveStr(KEY_ALIAS_URL); + if (StrUtil.isBlank(url)) { + throw new DbRuntimeException("No JDBC URL for group: [{}]", group); + } + // 自动识别Driver + String driver = config.getAndRemoveStr(KEY_ALIAS_DRIVER); + if (StrUtil.isBlank(driver)) { + driver = DriverUtil.identifyDriver(url); + } + final String user = config.getAndRemoveStr(KEY_ALIAS_USER); + final String pass = config.getAndRemoveStr(KEY_ALIAS_PASSWORD); + + return DataSourceWrapper.wrap(createDataSource(url, driver, user, pass, config), driver); + } + + /** + * 创建新的{@link DataSource}
+ * + * @param jdbcUrl JDBC连接字符串 + * @param driver 数据库驱动类名 + * @param user 用户名 + * @param pass 密码 + * @param poolSetting 分组下的连接池配置文件 + * @return {@link DataSource} + */ + protected abstract DataSource createDataSource(String jdbcUrl, String driver, String user, String pass, Setting poolSetting); + + @Override + public void close(String group) { + if (group == null) { + group = StrUtil.EMPTY; + } + + DataSourceWrapper ds = dsMap.get(group); + if (ds != null) { + ds.close(); + dsMap.remove(group); + } + } + + @Override + public void destroy() { + if (CollectionUtil.isNotEmpty(dsMap)) { + Collection values = dsMap.values(); + for (DataSourceWrapper ds : values) { + ds.close(); + } + dsMap.clear(); + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((dataSourceName == null) ? 0 : dataSourceName.hashCode()); + result = prime * result + ((setting == null) ? 0 : setting.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; + } + AbstractDSFactory other = (AbstractDSFactory) obj; + if (dataSourceName == null) { + if (other.dataSourceName != null) { + return false; + } + } else if (!dataSourceName.equals(other.dataSourceName)) { + return false; + } + if (setting == null) { + if (other.setting != null) { + return false; + } + } else if (!setting.equals(other.setting)) { + return false; + } + return true; + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/ds/DSFactory.java b/hutool-db/src/main/java/cn/hutool/db/ds/DSFactory.java new file mode 100644 index 000000000..ed80cc0f7 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/ds/DSFactory.java @@ -0,0 +1,192 @@ +package cn.hutool.db.ds; + +import java.io.Closeable; +import java.io.Serializable; + +import javax.sql.DataSource; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.db.ds.c3p0.C3p0DSFactory; +import cn.hutool.db.ds.dbcp.DbcpDSFactory; +import cn.hutool.db.ds.druid.DruidDSFactory; +import cn.hutool.db.ds.hikari.HikariDSFactory; +import cn.hutool.db.ds.pooled.PooledDSFactory; +import cn.hutool.db.ds.tomcat.TomcatDSFactory; +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import cn.hutool.setting.Setting; + +/** + * 抽象数据源工厂类
+ * 通过实现{@link #getDataSource(String)} 方法实现数据源的获取
+ * 如果{@link DataSource} 的实现是数据库连接池库,应该在getDataSource调用时创建数据源并缓存 + * + * @author Looly + * + */ +public abstract class DSFactory implements Closeable, Serializable{ + private static final long serialVersionUID = -8789780234095234765L; + + private static final Log log = LogFactory.get(); + + /** 别名字段名:URL */ + public static final String[] KEY_ALIAS_URL = { "url", "jdbcUrl" }; + /** 别名字段名:驱动名 */ + public static final String[] KEY_ALIAS_DRIVER = { "driver", "driverClassName" }; + /** 别名字段名:用户名 */ + public static final String[] KEY_ALIAS_USER = { "user", "username" }; + /** 别名字段名:密码 */ + public static final String[] KEY_ALIAS_PASSWORD = { "pass", "password" }; + + /** 数据源名 */ + protected final String dataSourceName; + + /** + * 构造 + * + * @param dataSourceName 数据源名称 + */ + public DSFactory(String dataSourceName) { + this.dataSourceName = dataSourceName; + } + + /** + * 获得默认数据源 + * + * @return 数据源 + */ + public DataSource getDataSource() { + return getDataSource(StrUtil.EMPTY); + } + + /** + * 获得分组对应数据源 + * + * @param group 分组名 + * @return 数据源 + */ + public abstract DataSource getDataSource(String group); + + /** + * 关闭默认数据源(空组) + */ + @Override + public void close() { + close(StrUtil.EMPTY); + } + + /** + * 关闭对应数据源 + * + * @param group 分组 + */ + public abstract void close(String group); + + /** + * 销毁工厂类,关闭所有数据源 + */ + public abstract void destroy(); + + // ------------------------------------------------------------------------- Static start + /** + * 获得数据源
+ * 使用默认配置文件的无分组配置 + * + * @return 数据源 + */ + public static DataSource get() { + return get(null); + } + + /** + * 获得数据源 + * + * @param group 配置文件中对应的分组 + * @return 数据源 + */ + public static DataSource get(String group) { + return GlobalDSFactory.get().getDataSource(group); + } + + /** + * 根据Setting获取当前数据源工厂对象 + * + * @param setting 数据源配置文件 + * @return 当前使用的数据源工厂 + * @deprecated 此方法容易引起歧义,应使用{@link #create(Setting)} 方法代替之 + */ + @Deprecated + public static DSFactory getCurrentDSFactory(Setting setting) { + return create(setting); + } + + /** + * 设置全局的数据源工厂
+ * 在项目中存在多个连接池库的情况下,我们希望使用低优先级的库时使用此方法自定义之
+ * 重新定义全局的数据源工厂此方法可在以下两种情况下调用: + * + *
+	 * 1. 在get方法调用前调用此方法来自定义全局的数据源工厂
+	 * 2. 替换已存在的全局数据源工厂,当已存在时会自动关闭
+	 * 
+ * + * @param dsFactory 数据源工厂 + * @return 自定义的数据源工厂 + */ + public static DSFactory setCurrentDSFactory(DSFactory dsFactory) { + return GlobalDSFactory.set(dsFactory); + } + + /** + * 创建数据源实现工厂
+ * 此方法通过“试错”方式查找引入项目的连接池库,按照优先级寻找,一旦寻找到则创建对应的数据源工厂
+ * 连接池优先级:Hikari > Druid > Tomcat > Dbcp > C3p0 > Hutool Pooled + * + * @return 日志实现类 + */ + public static DSFactory create(Setting setting) { + final DSFactory dsFactory = doCreate(setting); + log.debug("Use [{}] DataSource As Default", dsFactory.dataSourceName); + return dsFactory; + } + + /** + * 创建数据源实现工厂
+ * 此方法通过“试错”方式查找引入项目的连接池库,按照优先级寻找,一旦寻找到则创建对应的数据源工厂
+ * 连接池优先级:Hikari > Druid > Tomcat > Dbcp > C3p0 > Hutool Pooled + * + * @return 日志实现类 + * @since 4.1.3 + */ + private static DSFactory doCreate(Setting setting) { + try { + return new HikariDSFactory(setting); + } catch (NoClassDefFoundError e) { + // ignore + } + try { + return new DruidDSFactory(setting); + } catch (NoClassDefFoundError e) { + // ignore + } + try { + return new TomcatDSFactory(setting); + } catch (NoClassDefFoundError e) { + //如果未引入包,此处会报org.apache.tomcat.jdbc.pool.PoolConfiguration未找到错误 + //因为org.apache.tomcat.jdbc.pool.DataSource实现了此接口,会首先检查接口的存在与否 + // ignore + } + try { + return new DbcpDSFactory(setting); + } catch (NoClassDefFoundError e) { + // ignore + } + try { + return new C3p0DSFactory(setting); + } catch (NoClassDefFoundError e) { + // ignore + } + return new PooledDSFactory(setting); + } + // ------------------------------------------------------------------------- Static end +} diff --git a/hutool-db/src/main/java/cn/hutool/db/ds/DataSourceWrapper.java b/hutool-db/src/main/java/cn/hutool/db/ds/DataSourceWrapper.java new file mode 100644 index 000000000..999dd00a8 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/ds/DataSourceWrapper.java @@ -0,0 +1,120 @@ +package cn.hutool.db.ds; + +import java.io.Closeable; +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.logging.Logger; + +import javax.sql.DataSource; + +import cn.hutool.core.io.IoUtil; + +/** + * {@link DataSource} 数据源实现包装,通过包装,提供基本功能外的额外功能和参数持有,包括: + * + *
+ * 1. 提供驱动名的持有,用于确定数据库方言
+ * 
+ * + * @author looly + * @since 4.3.2 + */ +public class DataSourceWrapper implements DataSource, Closeable, Cloneable { + + private DataSource ds; + private String driver; + + /** + * 包装指定的DataSource + * + * @param ds 原始的DataSource + * @param driver 数据库驱动类名 + */ + public static DataSourceWrapper wrap(DataSource ds, String driver) { + return new DataSourceWrapper(ds, driver); + } + + /** + * 构造 + * + * @param ds 原始的DataSource + * @param driver 数据库驱动类名 + */ + public DataSourceWrapper(DataSource ds, String driver) { + this.ds = ds; + this.driver = driver; + } + + /** + * 获取驱动名 + * + * @return 驱动名 + */ + public String getDriver() { + return this.driver; + } + + /** + * 获取原始的数据源 + * + * @return 原始数据源 + */ + public DataSource getRaw() { + return this.ds; + } + + @Override + public PrintWriter getLogWriter() throws SQLException { + return ds.getLogWriter(); + } + + @Override + public void setLogWriter(PrintWriter out) throws SQLException { + ds.setLogWriter(out); + } + + @Override + public void setLoginTimeout(int seconds) throws SQLException { + ds.setLoginTimeout(seconds); + } + + @Override + public int getLoginTimeout() throws SQLException { + return ds.getLoginTimeout(); + } + + @Override + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + return ds.getParentLogger(); + } + + @Override + public T unwrap(Class iface) throws SQLException { + return ds.unwrap(iface); + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return ds.isWrapperFor(iface); + } + + @Override + public Connection getConnection() throws SQLException { + return ds.getConnection(); + } + + @Override + public Connection getConnection(String username, String password) throws SQLException { + return ds.getConnection(username, password); + } + + @Override + public void close() { + if (this.ds instanceof AutoCloseable) { + IoUtil.close((AutoCloseable) this.ds); + } + } + +} \ No newline at end of file diff --git a/hutool-db/src/main/java/cn/hutool/db/ds/GlobalDSFactory.java b/hutool-db/src/main/java/cn/hutool/db/ds/GlobalDSFactory.java new file mode 100644 index 000000000..66297fadd --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/ds/GlobalDSFactory.java @@ -0,0 +1,79 @@ +package cn.hutool.db.ds; + +import cn.hutool.log.StaticLog; + +/** + * 全局的数据源工厂
+ * 一般情况下,一个应用默认只使用一种数据库连接池,因此维护一个全局的数据源工厂类减少判断连接池类型造成的性能浪费 + * + * @author looly + * @since 4.0.2 + * + */ +public class GlobalDSFactory { + + private static volatile DSFactory factory; + private static Object lock = new Object(); + + /** + * 设置在JVM关闭时关闭所有数据库连接 + */ + static { + // JVM关闭时关闭所有连接池 + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + if (null != factory) { + factory.destroy(); + StaticLog.debug("DataSource: [{}] destroyed.", factory.dataSourceName); + factory = null; + } + } + }); + } + + /** + * 获取默认的数据源工厂,读取默认数据库配置文件
+ * 此处使用懒加载模式,在第一次调用此方法时才创建默认数据源工厂
+ * 如果想自定义全局的数据源工厂,请在第一次调用此方法前调用{@link #set(DSFactory)} 方法自行定义 + * + * @return 当前使用的数据源工厂 + */ + public static DSFactory get() { + if (null == factory) { + synchronized (lock) { + if (null == factory) { + factory = DSFactory.create(null); + } + } + } + return factory; + } + + /** + * 设置全局的数据源工厂
+ * 在项目中存在多个连接池库的情况下,我们希望使用低优先级的库时使用此方法自定义之
+ * 重新定义全局的数据源工厂此方法可在以下两种情况下调用: + * + *
+	 * 1. 在get方法调用前调用此方法来自定义全局的数据源工厂
+	 * 2. 替换已存在的全局数据源工厂,当已存在时会自动关闭
+	 * 
+ * + * @param customDSFactory 自定义数据源工厂 + * @return 自定义的数据源工厂 + */ + synchronized public static DSFactory set(DSFactory customDSFactory) { + if (null != factory) { + if (factory.equals(customDSFactory)) { + return factory;// 数据源工厂不变时返回原数据源工厂 + } + // 自定义数据源工厂前关闭之前的数据源 + factory.destroy(); + } + + StaticLog.debug("Custom use [{}] datasource.", customDSFactory.dataSourceName); + factory = customDSFactory; + return factory; + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/ds/c3p0/C3p0DSFactory.java b/hutool-db/src/main/java/cn/hutool/db/ds/c3p0/C3p0DSFactory.java new file mode 100644 index 000000000..4dd3e62d7 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/ds/c3p0/C3p0DSFactory.java @@ -0,0 +1,55 @@ +package cn.hutool.db.ds.c3p0; + +import java.beans.PropertyVetoException; + +import javax.sql.DataSource; + +import com.mchange.v2.c3p0.ComboPooledDataSource; + +import cn.hutool.db.DbRuntimeException; +import cn.hutool.db.ds.AbstractDSFactory; +import cn.hutool.setting.Setting; + +/** + * Druid数据源工厂类 + * + * @author Looly + * + */ +public class C3p0DSFactory extends AbstractDSFactory { + private static final long serialVersionUID = -6090788225842047281L; + + public static final String DS_NAME = "C3P0"; + + /** + * 构造,使用默认配置 + */ + public C3p0DSFactory() { + this(null); + } + + /** + * 构造 + * + * @param setting 配置 + */ + public C3p0DSFactory(Setting setting) { + super(DS_NAME, ComboPooledDataSource.class, setting); + } + + @Override + protected DataSource createDataSource(String jdbcUrl, String driver, String user, String pass, Setting poolSetting) { + final ComboPooledDataSource ds = new ComboPooledDataSource(); + ds.setJdbcUrl(jdbcUrl); + try { + ds.setDriverClass(driver); + } catch (PropertyVetoException e) { + throw new DbRuntimeException(e); + } + ds.setUser(user); + ds.setPassword(pass); + poolSetting.toBean(ds);// 注入属性 + + return ds; + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/ds/c3p0/package-info.java b/hutool-db/src/main/java/cn/hutool/db/ds/c3p0/package-info.java new file mode 100644 index 000000000..a6ca46ba5 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/ds/c3p0/package-info.java @@ -0,0 +1,7 @@ +/** + * C3P0封装 + * + * @author looly + * + */ +package cn.hutool.db.ds.c3p0; \ No newline at end of file diff --git a/hutool-db/src/main/java/cn/hutool/db/ds/dbcp/DbcpDSFactory.java b/hutool-db/src/main/java/cn/hutool/db/ds/dbcp/DbcpDSFactory.java new file mode 100644 index 000000000..65d2ae727 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/ds/dbcp/DbcpDSFactory.java @@ -0,0 +1,41 @@ +package cn.hutool.db.ds.dbcp; + +import javax.sql.DataSource; + +import org.apache.commons.dbcp2.BasicDataSource; + +import cn.hutool.db.ds.AbstractDSFactory; +import cn.hutool.setting.Setting; + +/** + * DBCP2数据源工厂类 + * + * @author Looly + * + */ +public class DbcpDSFactory extends AbstractDSFactory { + private static final long serialVersionUID = -9133501414334104548L; + + public static final String DS_NAME = "commons-dbcp2"; + + public DbcpDSFactory() { + this(null); + } + + public DbcpDSFactory(Setting setting) { + super(DS_NAME, BasicDataSource.class, setting); + } + + @Override + protected DataSource createDataSource(String jdbcUrl, String driver, String user, String pass, Setting poolSetting) { + final BasicDataSource ds = new BasicDataSource(); + + ds.setUrl(jdbcUrl); + ds.setDriverClassName(driver); + ds.setUsername(user); + ds.setPassword(pass); + poolSetting.toBean(ds);// 注入属性 + + return ds; + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/ds/dbcp/package-info.java b/hutool-db/src/main/java/cn/hutool/db/ds/dbcp/package-info.java new file mode 100644 index 000000000..d7e81a9c4 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/ds/dbcp/package-info.java @@ -0,0 +1,7 @@ +/** + * DBCP封装 + * + * @author looly + * + */ +package cn.hutool.db.ds.dbcp; \ No newline at end of file diff --git a/hutool-db/src/main/java/cn/hutool/db/ds/druid/DruidDSFactory.java b/hutool-db/src/main/java/cn/hutool/db/ds/druid/DruidDSFactory.java new file mode 100644 index 000000000..e1b45cb89 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/ds/druid/DruidDSFactory.java @@ -0,0 +1,70 @@ +package cn.hutool.db.ds.druid; + +import java.util.Map.Entry; +import java.util.Properties; + +import javax.sql.DataSource; + +import com.alibaba.druid.pool.DruidDataSource; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.db.ds.AbstractDSFactory; +import cn.hutool.setting.Setting; + +/** + * Druid数据源工厂类 + * + * @author Looly + * + */ +public class DruidDSFactory extends AbstractDSFactory { + private static final long serialVersionUID = 4680621702534433222L; + + public static final String DS_NAME = "Druid"; + + /** + * 构造,使用默认配置文件 + */ + public DruidDSFactory() { + this(null); + } + + /** + * 构造 + * + * @param setting 数据库配置 + */ + public DruidDSFactory(Setting setting) { + super(DS_NAME, DruidDataSource.class, setting); + } + + @Override + protected DataSource createDataSource(String jdbcUrl, String driver, String user, String pass, Setting poolSetting) { + final DruidDataSource ds = new DruidDataSource(); + + ds.setUrl(jdbcUrl); + ds.setDriverClassName(driver); + ds.setUsername(user); + ds.setPassword(pass); + + // 规范化属性名 + Properties druidProps = new Properties(); + String keyStr; + for (Entry entry : poolSetting.entrySet()) { + keyStr = StrUtil.addPrefixIfNot(entry.getKey(), "druid."); + druidProps.put(keyStr, entry.getValue()); + } + // 连接池信息 + ds.configFromPropety(druidProps); + + // 检查关联配置,在用户未设置某项配置时, + if (null == ds.getValidationQuery()) { + // 在validationQuery未设置的情况下,以下三项设置都将无效 + ds.setTestOnBorrow(false); + ds.setTestOnReturn(false); + ds.setTestWhileIdle(false); + } + + return ds; + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/ds/druid/package-info.java b/hutool-db/src/main/java/cn/hutool/db/ds/druid/package-info.java new file mode 100644 index 000000000..10d61ae2f --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/ds/druid/package-info.java @@ -0,0 +1,7 @@ +/** + * Druid封装 + * + * @author looly + * + */ +package cn.hutool.db.ds.druid; \ No newline at end of file diff --git a/hutool-db/src/main/java/cn/hutool/db/ds/hikari/HikariDSFactory.java b/hutool-db/src/main/java/cn/hutool/db/ds/hikari/HikariDSFactory.java new file mode 100644 index 000000000..063a7e708 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/ds/hikari/HikariDSFactory.java @@ -0,0 +1,50 @@ +package cn.hutool.db.ds.hikari; + +import javax.sql.DataSource; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +import cn.hutool.db.ds.AbstractDSFactory; +import cn.hutool.setting.Setting; +import cn.hutool.setting.dialect.Props; + +/** + * HikariCP数据源工厂类 + * + * @author Looly + * + */ +public class HikariDSFactory extends AbstractDSFactory { + private static final long serialVersionUID = -8834744983614749401L; + + public static final String DS_NAME = "HikariCP"; + + public HikariDSFactory() { + this(null); + } + + public HikariDSFactory(Setting setting) { + super(DS_NAME, HikariDataSource.class, setting); + } + + @Override + protected DataSource createDataSource(String jdbcUrl, String driver, String user, String pass, Setting poolSetting) { + final Props config = new Props(); + config.putAll(poolSetting); + + config.put("jdbcUrl", jdbcUrl); + if (null != driver) { + config.put("driverClassName", driver); + } + if (null != user) { + config.put("username", user); + } + if (null != pass) { + config.put("password", pass); + } + + final HikariDataSource ds = new HikariDataSource(new HikariConfig(config)); + return ds; + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/ds/hikari/package-info.java b/hutool-db/src/main/java/cn/hutool/db/ds/hikari/package-info.java new file mode 100644 index 000000000..ac5325c92 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/ds/hikari/package-info.java @@ -0,0 +1,7 @@ +/** + * Hikari封装 + * + * @author looly + * + */ +package cn.hutool.db.ds.hikari; \ No newline at end of file diff --git a/hutool-db/src/main/java/cn/hutool/db/ds/jndi/JndiDSFactory.java b/hutool-db/src/main/java/cn/hutool/db/ds/jndi/JndiDSFactory.java new file mode 100644 index 000000000..3bbd7c9e7 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/ds/jndi/JndiDSFactory.java @@ -0,0 +1,43 @@ +package cn.hutool.db.ds.jndi; + +import javax.sql.DataSource; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.db.DbRuntimeException; +import cn.hutool.db.DbUtil; +import cn.hutool.db.ds.AbstractDSFactory; +import cn.hutool.setting.Setting; + +/** + * JNDI数据源工厂类
+ * Setting配置样例:
+ * ---------------------
+ * [group]
+ * jndi = jdbc/TestDB
+ * ---------------------
+ * + * @author Looly + * + */ +public class JndiDSFactory extends AbstractDSFactory { + private static final long serialVersionUID = 1573625812927370432L; + + public static final String DS_NAME = "JNDI DataSource"; + + public JndiDSFactory() { + this(null); + } + + public JndiDSFactory(Setting setting) { + super(DS_NAME, null, setting); + } + + @Override + protected DataSource createDataSource(String jdbcUrl, String driver, String user, String pass, Setting poolSetting) { + String jndiName = poolSetting.getStr("jndi"); + if (StrUtil.isEmpty(jndiName)) { + throw new DbRuntimeException("No setting name [jndi] for this group."); + } + return DbUtil.getJndiDs(jndiName); + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/ds/jndi/package-info.java b/hutool-db/src/main/java/cn/hutool/db/ds/jndi/package-info.java new file mode 100644 index 000000000..b6340d498 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/ds/jndi/package-info.java @@ -0,0 +1,7 @@ +/** + * JNDI封装 + * + * @author looly + * + */ +package cn.hutool.db.ds.jndi; \ No newline at end of file diff --git a/hutool-db/src/main/java/cn/hutool/db/ds/package-info.java b/hutool-db/src/main/java/cn/hutool/db/ds/package-info.java new file mode 100644 index 000000000..edf4d4ab5 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/ds/package-info.java @@ -0,0 +1,7 @@ +/** + * 数据源封装,对各类数据库连接池的封装 + * + * @author looly + * + */ +package cn.hutool.db.ds; \ No newline at end of file diff --git a/hutool-db/src/main/java/cn/hutool/db/ds/pooled/ConnectionWraper.java b/hutool-db/src/main/java/cn/hutool/db/ds/pooled/ConnectionWraper.java new file mode 100644 index 000000000..49d3ed20f --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/ds/pooled/ConnectionWraper.java @@ -0,0 +1,297 @@ +package cn.hutool.db.ds.pooled; + +import java.sql.Array; +import java.sql.Blob; +import java.sql.CallableStatement; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.NClob; +import java.sql.PreparedStatement; +import java.sql.SQLClientInfoException; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Savepoint; +import java.sql.Statement; +import java.sql.Struct; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.Executor; + +/** + * 连接包装,用于丰富功能 + * @author Looly + * + */ +public abstract class ConnectionWraper implements Connection{ + + protected Connection raw;//真正的连接 + + @Override + public T unwrap(Class iface) throws SQLException { + return raw.unwrap(iface); + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return raw.isWrapperFor(iface); + } + + @Override + public Statement createStatement() throws SQLException { + return raw.createStatement(); + } + + @Override + public PreparedStatement prepareStatement(String sql) throws SQLException { + return raw.prepareStatement(sql); + } + + @Override + public CallableStatement prepareCall(String sql) throws SQLException { + return raw.prepareCall(sql); + } + + @Override + public String nativeSQL(String sql) throws SQLException { + return raw.nativeSQL(sql); + } + + @Override + public void setAutoCommit(boolean autoCommit) throws SQLException { + raw.setAutoCommit(autoCommit); + } + + @Override + public boolean getAutoCommit() throws SQLException { + return raw.getAutoCommit(); + } + + @Override + public void commit() throws SQLException { + raw.commit(); + } + + @Override + public void rollback() throws SQLException { + raw.rollback(); + } + + @Override + public DatabaseMetaData getMetaData() throws SQLException { + return raw.getMetaData(); + } + + @Override + public void setReadOnly(boolean readOnly) throws SQLException { + raw.setReadOnly(readOnly); + } + + @Override + public boolean isReadOnly() throws SQLException { + return raw.isReadOnly(); + } + + @Override + public void setCatalog(String catalog) throws SQLException { + raw.setCatalog(catalog); + } + + @Override + public String getCatalog() throws SQLException { + return raw.getCatalog(); + } + + @Override + public void setTransactionIsolation(int level) throws SQLException { + raw.setTransactionIsolation(level); + } + + @Override + public int getTransactionIsolation() throws SQLException { + return raw.getTransactionIsolation(); + } + + @Override + public SQLWarning getWarnings() throws SQLException { + return raw.getWarnings(); + } + + @Override + public void clearWarnings() throws SQLException { + raw.clearWarnings(); + } + + @Override + public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { + return raw.createStatement(resultSetType, resultSetConcurrency); + } + + @Override + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + return raw.prepareStatement(sql, resultSetType, resultSetConcurrency); + } + + @Override + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + return raw.prepareCall(sql, resultSetType, resultSetConcurrency); + } + + @Override + public Map> getTypeMap() throws SQLException { + return raw.getTypeMap(); + } + + @Override + public void setTypeMap(Map> map) throws SQLException { + raw.setTypeMap(map); + } + + @Override + public void setHoldability(int holdability) throws SQLException { + raw.setHoldability(holdability); + } + + @Override + public int getHoldability() throws SQLException { + return raw.getHoldability(); + } + + @Override + public Savepoint setSavepoint() throws SQLException { + return raw.setSavepoint(); + } + + @Override + public Savepoint setSavepoint(String name) throws SQLException { + return raw.setSavepoint(name); + } + + @Override + public void rollback(Savepoint savepoint) throws SQLException { + raw.rollback(savepoint); + } + + @Override + public void releaseSavepoint(Savepoint savepoint) throws SQLException { + raw.releaseSavepoint(savepoint); + } + + @Override + public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + return raw.createStatement(resultSetType, resultSetConcurrency, resultSetHoldability); + } + + @Override + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + return raw.prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability); + } + + @Override + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + return raw.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability); + } + + @Override + public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { + return raw.prepareStatement(sql, autoGeneratedKeys); + } + + @Override + public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { + return raw.prepareStatement(sql, columnIndexes); + } + + @Override + public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { + return raw.prepareStatement(sql, columnNames); + } + + @Override + public Clob createClob() throws SQLException { + return raw.createClob(); + } + + @Override + public Blob createBlob() throws SQLException { + return raw.createBlob(); + } + + @Override + public NClob createNClob() throws SQLException { + return raw.createNClob(); + } + + @Override + public SQLXML createSQLXML() throws SQLException { + return raw.createSQLXML(); + } + + @Override + public boolean isValid(int timeout) throws SQLException { + return raw.isValid(timeout); + } + + @Override + public void setClientInfo(String name, String value) throws SQLClientInfoException { + raw.setClientInfo(name, value); + } + + @Override + public void setClientInfo(Properties properties) throws SQLClientInfoException { + raw.setClientInfo(properties); + } + + @Override + public String getClientInfo(String name) throws SQLException { + return raw.getClientInfo(name); + } + + @Override + public Properties getClientInfo() throws SQLException { + return raw.getClientInfo(); + } + + @Override + public Array createArrayOf(String typeName, Object[] elements) throws SQLException { + return raw.createArrayOf(typeName, elements); + } + + @Override + public Struct createStruct(String typeName, Object[] attributes) throws SQLException { + return raw.createStruct(typeName, attributes); + } + + @Override + public void setSchema(String schema) throws SQLException { + raw.setSchema(schema); + } + + @Override + public String getSchema() throws SQLException { + return raw.getSchema(); + } + + @Override + public void abort(Executor executor) throws SQLException { + raw.abort(executor); + } + + @Override + public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { + raw.setNetworkTimeout(executor, milliseconds); + } + + @Override + public int getNetworkTimeout() throws SQLException { + return raw.getNetworkTimeout(); + } + + /** + * @return 实际的连接对象 + */ + public Connection getRaw(){ + return this.raw; + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/ds/pooled/DbConfig.java b/hutool-db/src/main/java/cn/hutool/db/ds/pooled/DbConfig.java new file mode 100644 index 000000000..cf68510bd --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/ds/pooled/DbConfig.java @@ -0,0 +1,109 @@ +package cn.hutool.db.ds.pooled; + +import cn.hutool.db.DbRuntimeException; +import cn.hutool.db.dialect.DriverUtil; + +/** + * 数据库配置 + * @author Looly + * + */ +public class DbConfig { + + //-------------------------------------------------------------------- Fields start + private String driver; //数据库驱动 + private String url; //jdbc url + private String user; //用户名 + private String pass; //密码 + + private int initialSize; //初始连接数 + private int minIdle; //最小闲置连接数 + private int maxActive; //最大活跃连接数 + private long maxWait; //获取连接的超时等待 + //-------------------------------------------------------------------- Fields end + + //-------------------------------------------------------------------- Constructor start + public DbConfig() { + } + + /** + * 构造 + * @param url jdbc url + * @param user 用户名 + * @param pass 密码 + */ + public DbConfig(String url, String user, String pass) { + init(url, user, pass); + } + //-------------------------------------------------------------------- Constructor end + + /** + * 初始化 + * @param url jdbc url + * @param user 用户名 + * @param pass 密码 + */ + public void init(String url, String user, String pass) { + this.url = url; + this.user = user; + this.pass = pass; + this.driver = DriverUtil.identifyDriver(url); + try { + Class.forName(this.driver); + } catch (ClassNotFoundException e) { + throw new DbRuntimeException(e, "Get jdbc driver from [{}] error!", url); + } + } + + //-------------------------------------------------------------------- Getters and Setters start + public String getDriver() { + return driver; + } + public void setDriver(String driver) { + this.driver = driver; + } + public String getUrl() { + return url; + } + public void setUrl(String url) { + this.url = url; + } + public String getUser() { + return user; + } + public void setUser(String user) { + this.user = user; + } + public String getPass() { + return pass; + } + public void setPass(String pass) { + this.pass = pass; + } + + public int getInitialSize() { + return initialSize; + } + public void setInitialSize(int initialSize) { + this.initialSize = initialSize; + } + public int getMinIdle() { + return minIdle; + } + public void setMinIdle(int minIdle) { + this.minIdle = minIdle; + } + public int getMaxActive() { + return maxActive; + } + public void setMaxActive(int maxActive) { + this.maxActive = maxActive; + } + public long getMaxWait() { + return maxWait; + } + public void setMaxWait(long maxWait) { + this.maxWait = maxWait; + } + //-------------------------------------------------------------------- Getters and Setters end +} \ No newline at end of file diff --git a/hutool-db/src/main/java/cn/hutool/db/ds/pooled/DbSetting.java b/hutool-db/src/main/java/cn/hutool/db/ds/pooled/DbSetting.java new file mode 100644 index 000000000..09b6485b0 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/ds/pooled/DbSetting.java @@ -0,0 +1,76 @@ +package cn.hutool.db.ds.pooled; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.db.DbRuntimeException; +import cn.hutool.db.dialect.DriverUtil; +import cn.hutool.db.ds.DSFactory; +import cn.hutool.setting.Setting; + +/** + * 数据库配置文件类,此类对应一个数据库配置文件 + * + * @author Looly + * + */ +public class DbSetting { + /** 默认的数据库连接配置文件路径 */ + public final static String DEFAULT_DB_CONFIG_PATH = "config/db.setting"; + + private Setting setting; + + /** + * 构造 + */ + public DbSetting() { + this(null); + } + + /** + * 构造 + * + * @param setting 数据库配置 + */ + public DbSetting(Setting setting) { + if (null == setting) { + this.setting = new Setting(DEFAULT_DB_CONFIG_PATH); + } else { + this.setting = setting; + } + } + + /** + * 获得数据库连接信息 + * + * @param group 分组 + * @return 分组 + */ + public DbConfig getDbConfig(String group) { + final Setting config = setting.getSetting(group); + if (CollectionUtil.isEmpty(config)) { + throw new DbRuntimeException("No Hutool pool config for group: [{}]", group); + } + + final DbConfig dbConfig = new DbConfig(); + + // 基本信息 + final String url = config.getAndRemoveStr(DSFactory.KEY_ALIAS_URL); + if (StrUtil.isBlank(url)) { + throw new DbRuntimeException("No JDBC URL for group: [{}]", group); + } + dbConfig.setUrl(url); + // 自动识别Driver + final String driver = config.getAndRemoveStr(DSFactory.KEY_ALIAS_DRIVER); + dbConfig.setDriver(StrUtil.isNotBlank(driver) ? driver : DriverUtil.identifyDriver(url)); + dbConfig.setUser(config.getAndRemoveStr(DSFactory.KEY_ALIAS_USER)); + dbConfig.setPass(config.getAndRemoveStr(DSFactory.KEY_ALIAS_PASSWORD)); + + // 连接池相关信息 + dbConfig.setInitialSize(setting.getInt("initialSize", group, 0)); + dbConfig.setMinIdle(setting.getInt("minIdle", group, 0)); + dbConfig.setMaxActive(setting.getInt("maxActive", group, 8)); + dbConfig.setMaxWait(setting.getLong("maxWait", group, 6000L)); + + return dbConfig; + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/ds/pooled/PooledConnection.java b/hutool-db/src/main/java/cn/hutool/db/ds/pooled/PooledConnection.java new file mode 100644 index 000000000..8c9f7063a --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/ds/pooled/PooledConnection.java @@ -0,0 +1,66 @@ +package cn.hutool.db.ds.pooled; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +import cn.hutool.db.DbUtil; + +/** + * 池化 + * @author Looly + * + */ +public class PooledConnection extends ConnectionWraper{ + + private PooledDataSource ds; + private boolean isClosed; + + public PooledConnection(PooledDataSource ds) throws SQLException { + this.ds = ds; + DbConfig config = ds.getConfig(); + this.raw = DriverManager.getConnection(config.getUrl(), config.getUser(), config.getPass()); + } + + public PooledConnection(PooledDataSource ds, Connection conn) { + this.ds = ds; + this.raw = conn; + } + + /** + * 重写关闭连接,实际操作是归还到连接池中 + */ + @Override + public void close() throws SQLException { + this.ds.free(this); + this.isClosed = true; + } + + /** + * 连接是否关闭,关闭条件:
+ * 1、被归还到池中 + * 2、实际连接已关闭 + */ + @Override + public boolean isClosed() throws SQLException { + return isClosed || raw.isClosed(); + } + + /** + * 打开连接 + * @return this + */ + protected PooledConnection open() { + this.isClosed = false; + return this; + } + + /** + * 释放连接 + * @return this + */ + protected PooledConnection release() { + DbUtil.close(this.raw); + return this; + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/ds/pooled/PooledDSFactory.java b/hutool-db/src/main/java/cn/hutool/db/ds/pooled/PooledDSFactory.java new file mode 100644 index 000000000..db3c378b2 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/ds/pooled/PooledDSFactory.java @@ -0,0 +1,43 @@ +package cn.hutool.db.ds.pooled; + +import javax.sql.DataSource; + +import cn.hutool.db.ds.AbstractDSFactory; +import cn.hutool.setting.Setting; + +/** + * Hutool自身实现的池化数据源工厂类 + * + * @author Looly + * + */ +public class PooledDSFactory extends AbstractDSFactory { + private static final long serialVersionUID = 8093886210895248277L; + + public static final String DS_NAME = "Hutool-Pooled-DataSource"; + + public PooledDSFactory() { + this(null); + } + + public PooledDSFactory(Setting setting) { + super(DS_NAME, PooledDataSource.class, setting); + } + + @Override + protected DataSource createDataSource(String jdbcUrl, String driver, String user, String pass, Setting poolSetting) { + final DbConfig dbConfig = new DbConfig(); + dbConfig.setUrl(jdbcUrl); + dbConfig.setDriver(driver); + dbConfig.setUser(user); + dbConfig.setPass(pass); + + // 连接池相关信息 + dbConfig.setInitialSize(poolSetting.getInt("initialSize", 0)); + dbConfig.setMinIdle(poolSetting.getInt("minIdle", 0)); + dbConfig.setMaxActive(poolSetting.getInt("maxActive", 8)); + dbConfig.setMaxWait(poolSetting.getLong("maxWait", 6000L)); + + return new PooledDataSource(dbConfig); + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/ds/pooled/PooledDataSource.java b/hutool-db/src/main/java/cn/hutool/db/ds/pooled/PooledDataSource.java new file mode 100644 index 000000000..ee0d04e08 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/ds/pooled/PooledDataSource.java @@ -0,0 +1,189 @@ +package cn.hutool.db.ds.pooled; + +import java.io.IOException; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.LinkedList; +import java.util.Queue; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.db.DbRuntimeException; +import cn.hutool.db.ds.simple.AbstractDataSource; + +/** + * 池化数据源 + * + * @author Looly + * + */ +public class PooledDataSource extends AbstractDataSource { + + private Queue freePool; + private int activeCount; // 活跃连接数 + + private DbConfig config; + + /** + * 获得一个数据源 + * + * @param group 数据源分组 + * @return {@link PooledDataSource} + */ + synchronized public static PooledDataSource getDataSource(String group) { + return new PooledDataSource(group); + } + + /** + * 获得一个数据源,使用空分组 + * + * @return {@link PooledDataSource} + */ + synchronized public static PooledDataSource getDataSource() { + return new PooledDataSource(); + } + + // -------------------------------------------------------------------- Constructor start + /** + * 构造,读取默认的配置文件和默认分组 + */ + public PooledDataSource() { + this(StrUtil.EMPTY); + } + + /** + * 构造,读取默认的配置文件 + * + * @param group 分组 + */ + public PooledDataSource(String group) { + this(new DbSetting(), group); + } + + /** + * 构造 + * + * @param setting 数据库配置文件对象 + * @param group 分组 + */ + public PooledDataSource(DbSetting setting, String group) { + this(setting.getDbConfig(group)); + } + + /** + * 构造 + * + * @param config 数据库配置 + */ + public PooledDataSource(DbConfig config) { + this.config = config; + freePool = new LinkedList(); + int initialSize = config.getInitialSize(); + try { + while (initialSize-- > 0) { + freePool.offer(newConnection()); + } + } catch (SQLException e) { + throw new DbRuntimeException(e); + } + } + // -------------------------------------------------------------------- Constructor start + + /** + * 从数据库连接池中获取数据库连接对象 + */ + @Override + public synchronized Connection getConnection() throws SQLException { + return getConnection(config.getMaxWait()); + } + + @Override + public Connection getConnection(String username, String password) throws SQLException { + throw new SQLException("Pooled DataSource is not allow to get special Connection!"); + } + + /** + * 释放连接,连接会被返回给连接池 + * + * @param conn 连接 + * @return 释放成功与否 + */ + protected synchronized boolean free(PooledConnection conn) { + activeCount--; + return freePool.offer(conn); + } + + /** + * 创建新连接 + * + * @return 新连接 + * @throws SQLException SQL异常 + */ + public PooledConnection newConnection() throws SQLException { + return new PooledConnection(this); + } + + public DbConfig getConfig() { + return config; + } + + /** + * 获取连接对象 + * + * @param wait 当池中无连接等待的毫秒数 + * @return 连接对象 + * @throws SQLException SQL异常 + */ + public PooledConnection getConnection(long wait) throws SQLException { + try { + return getConnectionDirect(); + } catch (Exception e) { + ThreadUtil.sleep(wait); + } + return getConnectionDirect(); + } + + @Override + synchronized public void close() throws IOException { + if (CollectionUtil.isNotEmpty(this.freePool)) { + for (PooledConnection pooledConnection : freePool) { + pooledConnection.release(); + this.freePool.clear(); + this.freePool = null; + } + } + } + + @Override + protected void finalize() throws Throwable { + IoUtil.close(this); + } + + /** + * 直接从连接池中获取连接,如果池中无连接直接抛出异常 + * + * @return PooledConnection + * @throws SQLException SQL异常 + */ + private PooledConnection getConnectionDirect() throws SQLException { + if (null == freePool) { + throw new SQLException("PooledDataSource is closed!"); + } + + final int maxActive = config.getMaxActive(); + if (maxActive <= 0 || maxActive < this.activeCount) { + // 超过最大使用限制 + throw new SQLException("In used Connection is more than Max Active."); + } + + PooledConnection conn = freePool.poll(); + if (null == conn || conn.open().isClosed()) { + conn = this.newConnection(); + } + activeCount++; + return conn; + } + +} diff --git a/hutool-db/src/main/java/cn/hutool/db/ds/pooled/package-info.java b/hutool-db/src/main/java/cn/hutool/db/ds/pooled/package-info.java new file mode 100644 index 000000000..a0b1efee5 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/ds/pooled/package-info.java @@ -0,0 +1,7 @@ +/** + * Hutool对连接池的简单实现 + * + * @author looly + * + */ +package cn.hutool.db.ds.pooled; \ No newline at end of file diff --git a/hutool-db/src/main/java/cn/hutool/db/ds/simple/AbstractDataSource.java b/hutool-db/src/main/java/cn/hutool/db/ds/simple/AbstractDataSource.java new file mode 100644 index 000000000..4b8c3efb7 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/ds/simple/AbstractDataSource.java @@ -0,0 +1,56 @@ +package cn.hutool.db.ds.simple; + +import java.io.Closeable; +import java.io.PrintWriter; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.logging.Logger; + +import javax.sql.DataSource; + +/** + * 数据源抽象实现 + * @author Looly + * + */ +public abstract class AbstractDataSource implements DataSource, Cloneable, Closeable{ + @Override + public PrintWriter getLogWriter() throws SQLException { + return DriverManager.getLogWriter(); + } + + @Override + public void setLogWriter(PrintWriter out) throws SQLException { + DriverManager.setLogWriter(out); + } + + @Override + public void setLoginTimeout(int seconds) throws SQLException { + DriverManager.setLoginTimeout(seconds); + } + + @Override + public int getLoginTimeout() throws SQLException { + return DriverManager.getLoginTimeout(); + } + + @Override + public T unwrap(Class iface) throws SQLException { + throw new SQLException("Can't support unwrap method!"); + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + throw new SQLException("Can't support isWrapperFor method!"); + } + + /** + * Support from JDK7 + * @since 1.7 + */ + @Override + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + throw new SQLFeatureNotSupportedException("DataSource can't support getParentLogger method!"); + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/ds/simple/SimpleDSFactory.java b/hutool-db/src/main/java/cn/hutool/db/ds/simple/SimpleDSFactory.java new file mode 100644 index 000000000..d841e2d1a --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/ds/simple/SimpleDSFactory.java @@ -0,0 +1,37 @@ +package cn.hutool.db.ds.simple; + +import javax.sql.DataSource; + +import cn.hutool.db.ds.AbstractDSFactory; +import cn.hutool.setting.Setting; + +/** + * 简单数据源工厂类 + * + * @author Looly + * + */ +public class SimpleDSFactory extends AbstractDSFactory { + private static final long serialVersionUID = 4738029988261034743L; + + public static final String DS_NAME = "Hutool-Simple-DataSource"; + + public SimpleDSFactory() { + this(null); + } + + public SimpleDSFactory(Setting setting) { + super(DS_NAME, SimpleDataSource.class, setting); + } + + @Override + protected DataSource createDataSource(String jdbcUrl, String driver, String user, String pass, Setting poolSetting) { + return new SimpleDataSource(// + jdbcUrl, // + user, // + pass, // + driver// + ); + } + +} diff --git a/hutool-db/src/main/java/cn/hutool/db/ds/simple/SimpleDataSource.java b/hutool-db/src/main/java/cn/hutool/db/ds/simple/SimpleDataSource.java new file mode 100644 index 000000000..4e6dcdac8 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/ds/simple/SimpleDataSource.java @@ -0,0 +1,197 @@ +package cn.hutool.db.ds.simple; + +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.db.DbRuntimeException; +import cn.hutool.db.dialect.DriverUtil; +import cn.hutool.db.ds.DSFactory; +import cn.hutool.setting.Setting; + +/*** + * 简易数据源,没有使用连接池,仅供测试或打开关闭连接非常少的场合使用! + * + * @author loolly + * + */ +public class SimpleDataSource extends AbstractDataSource { + + /** 默认的数据库连接配置文件路径 */ + public final static String DEFAULT_DB_CONFIG_PATH = "config/db.setting"; + + // -------------------------------------------------------------------- Fields start + private String driver; // 数据库驱动 + private String url; // jdbc url + private String user; // 用户名 + private String pass; // 密码 + // -------------------------------------------------------------------- Fields end + + /** + * 获得一个数据源 + * + * @param group 数据源分组 + * @return {@link SimpleDataSource} + */ + synchronized public static SimpleDataSource getDataSource(String group) { + return new SimpleDataSource(group); + } + + /** + * 获得一个数据源,无分组 + * + * @return {@link SimpleDataSource} + */ + synchronized public static SimpleDataSource getDataSource() { + return new SimpleDataSource(); + } + + // -------------------------------------------------------------------- Constructor start + /** + * 构造 + */ + public SimpleDataSource() { + this(null); + } + + /** + * 构造 + * + * @param group 数据库配置文件中的分组 + */ + public SimpleDataSource(String group) { + this(null, group); + } + + /** + * 构造 + * + * @param setting 数据库配置 + * @param group 数据库配置文件中的分组 + */ + public SimpleDataSource(Setting setting, String group) { + if (null == setting) { + setting = new Setting(DEFAULT_DB_CONFIG_PATH); + } + final Setting config = setting.getSetting(group); + if (CollectionUtil.isEmpty(config)) { + throw new DbRuntimeException("No DataSource config for group: [{}]", group); + } + + init(// + config.getAndRemoveStr(DSFactory.KEY_ALIAS_URL), // + config.getAndRemoveStr(DSFactory.KEY_ALIAS_USER), // + config.getAndRemoveStr(DSFactory.KEY_ALIAS_PASSWORD), // + config.getAndRemoveStr(DSFactory.KEY_ALIAS_DRIVER)// + ); + } + + /** + * 构造 + * + * @param url jdbc url + * @param user 用户名 + * @param pass 密码 + */ + public SimpleDataSource(String url, String user, String pass) { + init(url, user, pass); + } + + /** + * 构造 + * + * @param url jdbc url + * @param user 用户名 + * @param pass 密码 + * @param driver JDBC驱动类 + * @since 3.1.2 + */ + public SimpleDataSource(String url, String user, String pass, String driver) { + init(url, user, pass, driver); + } + // -------------------------------------------------------------------- Constructor end + + /** + * 初始化 + * + * @param url jdbc url + * @param user 用户名 + * @param pass 密码 + */ + public void init(String url, String user, String pass) { + init(url, user, pass, null); + } + + /** + * 初始化 + * + * @param url jdbc url + * @param user 用户名 + * @param pass 密码 + * @param driver JDBC驱动类,传入空则自动识别驱动类 + * @since 3.1.2 + */ + public void init(String url, String user, String pass, String driver) { + this.driver = StrUtil.isNotBlank(driver) ? driver : DriverUtil.identifyDriver(url); + try { + Class.forName(this.driver); + } catch (ClassNotFoundException e) { + throw new DbRuntimeException(e, "Get jdbc driver [{}] error!", driver); + } + this.url = url; + this.user = user; + this.pass = pass; + } + + // -------------------------------------------------------------------- Getters and Setters start + public String getDriver() { + return driver; + } + + public void setDriver(String driver) { + this.driver = driver; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getUser() { + return user; + } + + public void setUser(String user) { + this.user = user; + } + + public String getPass() { + return pass; + } + + public void setPass(String pass) { + this.pass = pass; + } + // -------------------------------------------------------------------- Getters and Setters end + + @Override + public Connection getConnection() throws SQLException { + return DriverManager.getConnection(this.url, this.user, this.pass); + } + + @Override + public Connection getConnection(String username, String password) throws SQLException { + return DriverManager.getConnection(this.url, username, password); + } + + @Override + public void close() throws IOException { + // Not need to close; + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/ds/simple/package-info.java b/hutool-db/src/main/java/cn/hutool/db/ds/simple/package-info.java new file mode 100644 index 000000000..959301ed3 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/ds/simple/package-info.java @@ -0,0 +1,7 @@ +/** + * JDBC中DriverManager简易封装 + * + * @author looly + * + */ +package cn.hutool.db.ds.simple; \ No newline at end of file diff --git a/hutool-db/src/main/java/cn/hutool/db/ds/tomcat/TomcatDSFactory.java b/hutool-db/src/main/java/cn/hutool/db/ds/tomcat/TomcatDSFactory.java new file mode 100644 index 000000000..139874cb2 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/ds/tomcat/TomcatDSFactory.java @@ -0,0 +1,46 @@ +package cn.hutool.db.ds.tomcat; + +import org.apache.tomcat.jdbc.pool.DataSource; +import org.apache.tomcat.jdbc.pool.PoolProperties; + +import cn.hutool.db.ds.AbstractDSFactory; +import cn.hutool.setting.Setting; + +/** + * Tomcat-Jdbc-Pool数据源工厂类 + * + * @author Looly + * + */ +public class TomcatDSFactory extends AbstractDSFactory { + private static final long serialVersionUID = 4925514193275150156L; + + public static final String DS_NAME = "Tomcat-Jdbc-Pool"; + + /** + * 构造 + */ + public TomcatDSFactory() { + this(null); + } + + /** + * 构造 + * + * @param setting Setting数据库配置 + */ + public TomcatDSFactory(Setting setting) { + super(DS_NAME, DataSource.class, setting); + } + + @Override + protected javax.sql.DataSource createDataSource(String jdbcUrl, String driver, String user, String pass, Setting poolSetting) { + final PoolProperties poolProps = new PoolProperties(); + poolProps.setUrl(jdbcUrl); + poolProps.setDriverClassName(driver); + poolProps.setUsername(user); + poolProps.setPassword(pass); + + return new DataSource(poolProps); + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/ds/tomcat/package-info.java b/hutool-db/src/main/java/cn/hutool/db/ds/tomcat/package-info.java new file mode 100644 index 000000000..892133991 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/ds/tomcat/package-info.java @@ -0,0 +1,7 @@ +/** + * Tomcat-Pool封装 + * + * @author looly + * + */ +package cn.hutool.db.ds.tomcat; \ No newline at end of file diff --git a/hutool-db/src/main/java/cn/hutool/db/handler/BeanHandler.java b/hutool-db/src/main/java/cn/hutool/db/handler/BeanHandler.java new file mode 100644 index 000000000..74634960d --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/handler/BeanHandler.java @@ -0,0 +1,40 @@ +package cn.hutool.db.handler; + +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; + +/** + * Bean对象处理器,只处理第一条数据 + * + * @param 处理对象类型 + * @author loolly + *@since 3.1.0 + */ +public class BeanHandler implements RsHandler{ + private static final long serialVersionUID = -5491214744966544475L; + + private Class elementBeanType; + + /** + * 创建一个 BeanHandler对象 + * + * @param 处理对象类型 + * @param beanType Bean类型 + * @return BeanHandler对象 + */ + public static BeanHandler create(Class beanType) { + return new BeanHandler(beanType); + } + + public BeanHandler(Class beanType) { + this.elementBeanType = beanType; + } + + @Override + public E handle(ResultSet rs) throws SQLException { + final ResultSetMetaData meta = rs.getMetaData(); + final int columnCount = meta.getColumnCount(); + return rs.next() ? HandleHelper.handleRow(columnCount, meta, rs, this.elementBeanType) : null; + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/handler/BeanListHandler.java b/hutool-db/src/main/java/cn/hutool/db/handler/BeanListHandler.java new file mode 100644 index 000000000..3753db179 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/handler/BeanListHandler.java @@ -0,0 +1,43 @@ +package cn.hutool.db.handler; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * 结果集处理类 ,处理出的结果为Bean列表 + * + * @param 处理对象类型 + * @author loolly + * @since 3.1.0 + */ +public class BeanListHandler implements RsHandler> { + private static final long serialVersionUID = 4510569754766197707L; + + private Class elementBeanType; + + /** + * 创建一个 BeanListHandler对象 + * + * @param 处理对象类型 + * @param beanType Bean类型 + * @return BeanListHandler对象 + */ + public static BeanListHandler create(Class beanType) { + return new BeanListHandler(beanType); + } + + /** + * 构造 + * @param beanType Bean类型 + */ + public BeanListHandler(Class beanType) { + this.elementBeanType = beanType; + } + + @Override + public List handle(ResultSet rs) throws SQLException { + return HandleHelper.handleRsToBeanList(rs, new ArrayList(), elementBeanType); + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/handler/EntityHandler.java b/hutool-db/src/main/java/cn/hutool/db/handler/EntityHandler.java new file mode 100644 index 000000000..f1bf33e65 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/handler/EntityHandler.java @@ -0,0 +1,33 @@ +package cn.hutool.db.handler; + +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; + +import cn.hutool.db.Entity; + +/** + * Entity对象处理器,只处理第一条数据 + * + * @author loolly + * + */ +public class EntityHandler implements RsHandler{ + private static final long serialVersionUID = -8742432871908355992L; + + /** + * 创建一个 EntityHandler对象 + * @return EntityHandler对象 + */ + public static EntityHandler create() { + return new EntityHandler(); + } + + @Override + public Entity handle(ResultSet rs) throws SQLException { + final ResultSetMetaData meta = rs.getMetaData(); + final int columnCount = meta.getColumnCount(); + + return rs.next() ? HandleHelper.handleRow(columnCount, meta, rs) : null; + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/handler/EntityListHandler.java b/hutool-db/src/main/java/cn/hutool/db/handler/EntityListHandler.java new file mode 100644 index 000000000..1ec64edf2 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/handler/EntityListHandler.java @@ -0,0 +1,49 @@ +package cn.hutool.db.handler; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import cn.hutool.db.Entity; + +/** + * 结果集处理类 ,处理出的结果为Entity列表 + * @author loolly + * + */ +public class EntityListHandler implements RsHandler>{ + private static final long serialVersionUID = -2846240126316979895L; + + /** 是否大小写不敏感 */ + private boolean caseInsensitive; + + /** + * 创建一个 EntityListHandler对象 + * @return EntityListHandler对象 + */ + public static EntityListHandler create() { + return new EntityListHandler(); + } + + /** + * 构造 + */ + public EntityListHandler() { + this(false); + } + + /** + * 构造 + * + * @param caseInsensitive 是否大小写不敏感 + */ + public EntityListHandler(boolean caseInsensitive) { + this.caseInsensitive = caseInsensitive; + } + + @Override + public List handle(ResultSet rs) throws SQLException { + return HandleHelper.handleRs(rs, new ArrayList(), this.caseInsensitive); + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/handler/EntitySetHandler.java b/hutool-db/src/main/java/cn/hutool/db/handler/EntitySetHandler.java new file mode 100644 index 000000000..bca69e94e --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/handler/EntitySetHandler.java @@ -0,0 +1,29 @@ +package cn.hutool.db.handler; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.LinkedHashSet; + +import cn.hutool.db.Entity; + +/** + * 结果集处理类 ,处理出的结果为Entity列表,结果不能重复(按照Entity对象去重) + * @author loolly + * + */ +public class EntitySetHandler implements RsHandler>{ + private static final long serialVersionUID = 8191723216703506736L; + + /** + * 创建一个 EntityHandler对象 + * @return EntityHandler对象 + */ + public static EntitySetHandler create() { + return new EntitySetHandler(); + } + + @Override + public LinkedHashSet handle(ResultSet rs) throws SQLException { + return HandleHelper.handleRs(rs, new LinkedHashSet()); + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/handler/HandleHelper.java b/hutool-db/src/main/java/cn/hutool/db/handler/HandleHelper.java new file mode 100644 index 000000000..8f037d112 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/handler/HandleHelper.java @@ -0,0 +1,299 @@ +package cn.hutool.db.handler; + +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Collection; +import java.util.Map; + +import cn.hutool.core.bean.BeanDesc.PropDesc; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.TypeUtil; +import cn.hutool.db.Entity; + +/** + * 数据结果集处理辅助类 + * + * @author loolly + * + */ +public class HandleHelper { + + /** + * 处理单条数据 + * + * @param columnCount 列数 + * @param meta ResultSetMetaData + * @param rs 数据集 + * @param bean 目标Bean + * @return 每一行的Entity + * @throws SQLException SQL执行异常 + * @since 3.3.1 + */ + public static T handleRow(int columnCount, ResultSetMetaData meta, ResultSet rs, T bean) throws SQLException { + return handleRow(columnCount, meta, rs).toBeanIgnoreCase(bean); + } + + /** + * 处理单条数据 + * + * @param columnCount 列数 + * @param meta ResultSetMetaData + * @param rs 数据集 + * @param beanClass 目标Bean类型 + * @return 每一行的Entity + * @throws SQLException SQL执行异常 + * @since 3.3.1 + */ + @SuppressWarnings("unchecked") + public static T handleRow(int columnCount, ResultSetMetaData meta, ResultSet rs, Class beanClass) throws SQLException { + Assert.notNull(beanClass, "Bean Class must be not null !"); + + if(beanClass.isArray()) { + //返回数组 + final Class componentType = beanClass.getComponentType(); + final Object[] result = ArrayUtil.newArray(componentType, columnCount); + for(int i = 0,j = 1; i < columnCount; i++, j++) { + result[i] = getColumnValue(rs, j, meta.getColumnType(j), componentType); + } + return (T) result; + } else if(Iterable.class.isAssignableFrom(beanClass)) { + //集合 + final Object[] objRow = handleRow(columnCount, meta, rs, Object[].class); + return Convert.convert(beanClass, objRow); + } else if(beanClass.isAssignableFrom(Entity.class)) { + //Entity的父类都可按照Entity返回 + return (T) handleRow(columnCount, meta, rs); + } else if(String.class == beanClass) { + //字符串 + final Object[] objRow = handleRow(columnCount, meta, rs, Object[].class); + return (T) StrUtil.join(", ", objRow); + } + + //普通bean + final T bean = ReflectUtil.newInstanceIfPossible(beanClass); + //忽略字段大小写 + final Map propMap = BeanUtil.getBeanDesc(beanClass).getPropMap(true); + String columnLabel; + PropDesc pd; + Method setter = null; + Object value = null; + for (int i = 1; i <= columnCount; i++) { + columnLabel = meta.getColumnLabel(i); + pd = propMap.get(columnLabel); + if(null == pd) { + // 尝试驼峰命名风格 + pd = propMap.get(StrUtil.toCamelCase(columnLabel)); + } + setter = (null == pd) ? null : pd.getSetter(); + if(null != setter) { + value = getColumnValue(rs, columnLabel, meta.getColumnType(i), TypeUtil.getFirstParamType(setter)); + ReflectUtil.invokeWithCheck(bean, setter, new Object[] {value}); + } + } + return bean; + } + + /** + * 处理单条数据 + * + * @param columnCount 列数 + * @param meta ResultSetMetaData + * @param rs 数据集 + * @return 每一行的Entity + * @throws SQLException SQL执行异常 + */ + public static Entity handleRow(int columnCount, ResultSetMetaData meta, ResultSet rs) throws SQLException { + return handleRow(columnCount, meta, rs, false); + } + + /** + * 处理单条数据 + * + * @param columnCount 列数 + * @param meta ResultSetMetaData + * @param rs 数据集 + * @param caseInsensitive 是否大小写不敏感 + * @return 每一行的Entity + * @throws SQLException SQL执行异常 + * @since 4.5.16 + */ + public static Entity handleRow(int columnCount, ResultSetMetaData meta, ResultSet rs, boolean caseInsensitive) throws SQLException { + return handleRow(new Entity(null, caseInsensitive), columnCount, meta, rs, true); + } + + /** + * 处理单条数据 + * + * @param Entity及其子对象 + * @param row Entity对象 + * @param columnCount 列数 + * @param meta ResultSetMetaData + * @param rs 数据集 + * @param withMetaInfo 是否包含表名、字段名等元信息 + * @return 每一行的Entity + * @throws SQLException SQL执行异常 + * @since 3.3.1 + */ + public static T handleRow(T row, int columnCount, ResultSetMetaData meta, ResultSet rs, boolean withMetaInfo) throws SQLException { + String columnLabel; + int type; + for (int i = 1; i <= columnCount; i++) { + columnLabel = meta.getColumnLabel(i); + type = meta.getColumnType(i); + row.put(columnLabel, getColumnValue(rs, columnLabel, type, null)); + } + if (withMetaInfo) { + row.setTableName(meta.getTableName(1)); + row.setFieldNames(row.keySet()); + } + return row; + } + + /** + * 处理单条数据 + * + * @param rs 数据集 + * @return 每一行的Entity + * @throws SQLException SQL执行异常 + */ + public static Entity handleRow(ResultSet rs) throws SQLException { + final ResultSetMetaData meta = rs.getMetaData(); + final int columnCount = meta.getColumnCount(); + return handleRow(columnCount, meta, rs); + } + + /** + * 处理多条数据 + * + * @param 集合类型 + * @param rs 数据集 + * @param collection 数据集 + * @return Entity列表 + * @throws SQLException SQL执行异常 + */ + public static > T handleRs(ResultSet rs, T collection) throws SQLException { + return handleRs(rs, collection, false); + } + + /** + * 处理多条数据 + * + * @param 集合类型 + * @param rs 数据集 + * @param collection 数据集 + * @param caseInsensitive 是否大小写不敏感 + * @return Entity列表 + * @throws SQLException SQL执行异常 + * @since 4.5.16 + */ + public static > T handleRs(ResultSet rs, T collection, boolean caseInsensitive) throws SQLException { + final ResultSetMetaData meta = rs.getMetaData(); + final int columnCount = meta.getColumnCount(); + + while (rs.next()) { + collection.add(HandleHelper.handleRow(columnCount, meta, rs, caseInsensitive)); + } + + return collection; + } + + /** + * 处理多条数据并返回一个Bean列表 + * + * @param 集合元素类型 + * @param 集合类型 + * @param rs 数据集 + * @param collection 数据集 + * @param elementBeanType Bean类型 + * @return Entity列表 + * @throws SQLException SQL执行异常 + * @since 3.1.0 + */ + public static > T handleRsToBeanList(ResultSet rs, T collection, Class elementBeanType) throws SQLException { + final ResultSetMetaData meta = rs.getMetaData(); + final int columnCount = meta.getColumnCount(); + + while (rs.next()) { + collection.add(handleRow(columnCount, meta, rs, elementBeanType)); + } + + return collection; + } + + // -------------------------------------------------------------------------------------------------------------- Private method start + /** + * 获取字段值
+ * 针对日期时间等做单独处理判断 + * + * @param 返回类型 + * @param rs {@link ResultSet} + * @param label 字段标签或者字段名 + * @param type 字段类型,默认Object + * @param targetColumnType 结果要求的类型,需进行二次转换(null或者Object不转换) + * @return 字段值 + * @throws SQLException SQL异常 + */ + private static Object getColumnValue(ResultSet rs, String label, int type, Type targetColumnType) throws SQLException { + Object rawValue; + switch (type) { + case Types.TIMESTAMP: + rawValue = rs.getTimestamp(label); + break; + case Types.TIME: + rawValue = rs.getTime(label); + break; + default: + rawValue = rs.getObject(label); + } + if (null == targetColumnType || Object.class == targetColumnType) { + // 无需转换 + return rawValue; + } else { + // 按照返回值要求转换 + return Convert.convert(targetColumnType, rawValue); + } + } + + /** + * 获取字段值
+ * 针对日期时间等做单独处理判断 + * + * @param 返回类型 + * @param rs {@link ResultSet} + * @param columnIndex 字段索引 + * @param type 字段类型,默认Object + * @param targetColumnType 结果要求的类型,需进行二次转换(null或者Object不转换) + * @return 字段值 + * @throws SQLException SQL异常 + */ + private static Object getColumnValue(ResultSet rs, int columnIndex, int type, Type targetColumnType) throws SQLException { + Object rawValue; + switch (type) { + case Types.TIMESTAMP: + rawValue = rs.getTimestamp(columnIndex); + break; + case Types.TIME: + rawValue = rs.getTime(columnIndex); + break; + default: + rawValue = rs.getObject(columnIndex); + } + if (null == targetColumnType || Object.class == targetColumnType) { + // 无需转换 + return rawValue; + } else { + // 按照返回值要求转换 + return Convert.convert(targetColumnType, rawValue); + } + } + // -------------------------------------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-db/src/main/java/cn/hutool/db/handler/NumberHandler.java b/hutool-db/src/main/java/cn/hutool/db/handler/NumberHandler.java new file mode 100644 index 000000000..eb5b78a91 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/handler/NumberHandler.java @@ -0,0 +1,26 @@ +package cn.hutool.db.handler; + +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * 处理为数字结果,当查询结果为单个数字时使用此处理器(例如select count(1)) + * @author loolly + * + */ +public class NumberHandler implements RsHandler{ + private static final long serialVersionUID = 4081498054379705596L; + + /** + * 创建一个 NumberHandler对象 + * @return NumberHandler对象 + */ + public static NumberHandler create() { + return new NumberHandler(); + } + + @Override + public Number handle(ResultSet rs) throws SQLException { + return rs.next() ? rs.getBigDecimal(1) : null; + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/handler/PageResultHandler.java b/hutool-db/src/main/java/cn/hutool/db/handler/PageResultHandler.java new file mode 100644 index 000000000..c3ce8286d --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/handler/PageResultHandler.java @@ -0,0 +1,42 @@ +package cn.hutool.db.handler; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import cn.hutool.db.Entity; +import cn.hutool.db.PageResult; + +/** + * 分页结果集处理类 ,处理出的结果为PageResult + * @author loolly + * + */ +public class PageResultHandler implements RsHandler>{ + private static final long serialVersionUID = -1474161855834070108L; + + private PageResult pageResult; + + /** + * 创建一个 EntityHandler对象
+ * 结果集根据给定的分页对象查询数据库,填充结果 + * @param pageResult 分页结果集空对象 + * @return EntityHandler对象 + */ + public static PageResultHandler create(PageResult pageResult) { + return new PageResultHandler(pageResult); + } + + /** + * 构造
+ * 结果集根据给定的分页对象查询数据库,填充结果 + * @param pageResult 分页结果集空对象 + */ + public PageResultHandler(PageResult pageResult) { + this.pageResult = pageResult; + } + + @Override + public PageResult handle(ResultSet rs) throws SQLException { + return HandleHelper.handleRs(rs, pageResult); + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/handler/RsHandler.java b/hutool-db/src/main/java/cn/hutool/db/handler/RsHandler.java new file mode 100644 index 000000000..46262b897 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/handler/RsHandler.java @@ -0,0 +1,32 @@ +package cn.hutool.db.handler; + +import java.io.Serializable; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * 结果集处理接口
+ * 此接口用于实现{@link ResultSet} 转换或映射为用户指定的pojo对象 + * + * 默认实现有: + * @see EntityHandler + * @see EntityListHandler + * @see EntitySetHandler + * @see EntitySetHandler + * @see NumberHandler + * @see PageResultHandler + * + * @author Luxiaolei + * + */ +public interface RsHandler extends Serializable{ + + /** + * 处理结果集
+ * 结果集处理后不需要关闭 + * @param rs 结果集 + * @return 处理后生成的对象 + * @throws SQLException SQL异常 + */ + public T handle(ResultSet rs) throws SQLException; +} diff --git a/hutool-db/src/main/java/cn/hutool/db/handler/StringHandler.java b/hutool-db/src/main/java/cn/hutool/db/handler/StringHandler.java new file mode 100644 index 000000000..410d98439 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/handler/StringHandler.java @@ -0,0 +1,26 @@ +package cn.hutool.db.handler; + +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * 处理为字符串结果,当查询结果为单个字符串时使用此处理器 + * + * @author weibaohui + */ +public class StringHandler implements RsHandler{ + private static final long serialVersionUID = -5296733366845720383L; + + /** + * 创建一个 NumberHandler对象 + * @return NumberHandler对象 + */ + public static StringHandler create() { + return new StringHandler(); + } + + @Override + public String handle(ResultSet rs) throws SQLException { + return rs.next() ? rs.getString(1) : null; + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/handler/package-info.java b/hutool-db/src/main/java/cn/hutool/db/handler/package-info.java new file mode 100644 index 000000000..00ef17a50 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/handler/package-info.java @@ -0,0 +1,7 @@ +/** + * JDBC结果集(ResultSet)转换封装,通过实现RsHandler接口,将ResultSet转换为我们想要的数据类型 + * + * @author looly + * + */ +package cn.hutool.db.handler; \ No newline at end of file diff --git a/hutool-db/src/main/java/cn/hutool/db/meta/Column.java b/hutool-db/src/main/java/cn/hutool/db/meta/Column.java new file mode 100644 index 000000000..eb4285a04 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/meta/Column.java @@ -0,0 +1,244 @@ +package cn.hutool.db.meta; + +import java.io.Serializable; +import java.sql.ResultSet; +import java.sql.SQLException; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.db.DbRuntimeException; + +/** + * 数据库表的列信息 + * + * @author loolly + * + */ +public class Column implements Serializable, Cloneable { + private static final long serialVersionUID = 577527740359719367L; + + // ----------------------------------------------------- Fields start + /** 表名 */ + private String tableName; + + /** 列名 */ + private String name; + /** 类型,对应java.sql.Types中的类型 */ + private int type; + /** 类型名称 */ + private String typeName; + /** 大小或数据长度 */ + private int size; + /** 是否为可空 */ + private boolean isNullable; + /** 注释 */ + private String comment; + // ----------------------------------------------------- Fields end + + /** + * 创建列对象 + * + * @param tableName 表名 + * @param columnMetaRs 列元信息的ResultSet + * @return 列对象 + */ + public static Column create(String tableName, ResultSet columnMetaRs) { + return new Column(tableName, columnMetaRs); + } + + // ----------------------------------------------------- Constructor start + /** + * 构造 + */ + public Column() { + } + + /** + * 构造 + * + * @param tableName 表名 + * @param columnMetaRs Meta信息的ResultSet + */ + public Column(String tableName, ResultSet columnMetaRs) { + try { + init(tableName, columnMetaRs); + } catch (SQLException e) { + throw new DbRuntimeException(StrUtil.format("Get table [{}] meta info error!", tableName)); + } + } + // ----------------------------------------------------- Constructor end + + /** + * 初始化 + * + * @param tableName 表名 + * @param columnMetaRs 列的meta ResultSet + * @throws SQLException SQL执行异常 + */ + public void init(String tableName, ResultSet columnMetaRs) throws SQLException { + this.tableName = tableName; + + this.name = columnMetaRs.getString("COLUMN_NAME"); + this.type = columnMetaRs.getInt("DATA_TYPE"); + this.typeName = columnMetaRs.getString("TYPE_NAME"); + this.size = columnMetaRs.getInt("COLUMN_SIZE"); + this.isNullable = columnMetaRs.getBoolean("NULLABLE"); + this.comment = columnMetaRs.getString("REMARKS"); + } + + // ----------------------------------------------------- Getters and Setters start + /** + * 获取表名 + * + * @return 表名 + */ + public String getTableName() { + return tableName; + } + + /** + * 设置表名 + * + * @param tableName 表名 + * @return this + */ + public Column setTableName(String tableName) { + this.tableName = tableName; + return this; + } + + /** + * 获取列名 + * + * @return 列名 + */ + public String getName() { + return name; + } + + /** + * 设置列名 + * + * @param name 列名 + * @return this + */ + public Column setName(String name) { + this.name = name; + return this; + } + + /** + * 获取字段类型的枚举 + * + * @return 阻断类型枚举 + * @since 4.5.8 + */ + public JdbcType getTypeEnum() { + return JdbcType.valueOf(this.type); + } + + /** + * 获取类型,对应{@link java.sql.Types}中的类型 + * + * @return 类型 + */ + public int getType() { + return type; + } + + /** + * 设置类型,对应java.sql.Types中的类型 + * + * @param type 类型 + * @return this + */ + public Column setType(int type) { + this.type = type; + return this; + } + + /** + * 获取类型名称 + * + * @return 类型名称 + */ + public String getTypeName() { + return typeName; + } + + /** + * 设置类型名称 + * + * @param typeName 类型名称 + * @return this + */ + public Column setTypeName(String typeName) { + this.typeName = typeName; + return this; + } + + /** + * 获取大小或数据长度 + * + * @return 大小或数据长度 + */ + public int getSize() { + return size; + } + + /** + * 设置大小或数据长度 + * + * @param size 大小或数据长度 + * @return this + */ + public Column setSize(int size) { + this.size = size; + return this; + } + + /** + * 是否为可空 + * + * @return 是否为可空 + */ + public boolean isNullable() { + return isNullable; + } + + /** + * 设置是否为可空 + * + * @param isNullable 是否为可空 + * @return this + */ + public Column setNullable(boolean isNullable) { + this.isNullable = isNullable; + return this; + } + + /** + * 获取注释 + * + * @return 注释 + */ + public String getComment() { + return comment; + } + + /** + * 设置注释 + * + * @param comment 注释 + * @return this + */ + public Column setComment(String comment) { + this.comment = comment; + return this; + } + // ----------------------------------------------------- Getters and Setters end + + @Override + public String toString() { + return "Column [tableName=" + tableName + ", name=" + name + ", type=" + type + ", size=" + size + ", isNullable=" + isNullable + "]"; + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/meta/JdbcType.java b/hutool-db/src/main/java/cn/hutool/db/meta/JdbcType.java new file mode 100644 index 000000000..e980ab615 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/meta/JdbcType.java @@ -0,0 +1,80 @@ +package cn.hutool.db.meta; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author Clinton Begin + */ +public enum JdbcType { + ARRAY(java.sql.Types.ARRAY), // + BIT(java.sql.Types.BIT), // + TINYINT(java.sql.Types.TINYINT), // + SMALLINT(java.sql.Types.SMALLINT), // + INTEGER(java.sql.Types.INTEGER), // + BIGINT(java.sql.Types.BIGINT), // + FLOAT(java.sql.Types.FLOAT), // + REAL(java.sql.Types.REAL), // + DOUBLE(java.sql.Types.DOUBLE), // + NUMERIC(java.sql.Types.NUMERIC), // + DECIMAL(java.sql.Types.DECIMAL), // + CHAR(java.sql.Types.CHAR), // + VARCHAR(java.sql.Types.VARCHAR), // + LONGVARCHAR(java.sql.Types.LONGVARCHAR), // + DATE(java.sql.Types.DATE), // + TIME(java.sql.Types.TIME), // + TIMESTAMP(java.sql.Types.TIMESTAMP), // + BINARY(java.sql.Types.BINARY), // + VARBINARY(java.sql.Types.VARBINARY), // + LONGVARBINARY(java.sql.Types.LONGVARBINARY), // + NULL(java.sql.Types.NULL), // + OTHER(java.sql.Types.OTHER), // + BLOB(java.sql.Types.BLOB), // + CLOB(java.sql.Types.CLOB), // + BOOLEAN(java.sql.Types.BOOLEAN), // + CURSOR(-10), // Oracle + UNDEFINED(Integer.MIN_VALUE + 1000), // + NVARCHAR(java.sql.Types.NVARCHAR), // JDK6 + NCHAR(java.sql.Types.NCHAR), // JDK6 + NCLOB(java.sql.Types.NCLOB), // JDK6 + STRUCT(java.sql.Types.STRUCT), // + JAVA_OBJECT(java.sql.Types.JAVA_OBJECT), // + DISTINCT(java.sql.Types.DISTINCT), // + REF(java.sql.Types.REF), // + DATALINK(java.sql.Types.DATALINK), // + ROWID(java.sql.Types.ROWID), // JDK6 + LONGNVARCHAR(java.sql.Types.LONGNVARCHAR), // JDK6 + SQLXML(java.sql.Types.SQLXML), // JDK6 + DATETIMEOFFSET(-155), // SQL Server 2008 + TIME_WITH_TIMEZONE(2013), // JDBC 4.2 JDK8 + TIMESTAMP_WITH_TIMEZONE(2014); // JDBC 4.2 JDK8 + + public final int typeCode; + + /** + * 构造 + * + * @param code {@link java.sql.Types} 中对应的值 + */ + JdbcType(int code) { + this.typeCode = code; + } + + private static Map codeMap = new ConcurrentHashMap<>(100, 1); + static { + for (JdbcType type : JdbcType.values()) { + codeMap.put(type.typeCode, type); + } + } + + /** + * 通过{@link java.sql.Types}中对应int值找到enum值 + * + * @param code Jdbc type值 + * @return {@link JdbcType} + */ + public static JdbcType valueOf(int code) { + return codeMap.get(code); + } + +} diff --git a/hutool-db/src/main/java/cn/hutool/db/meta/MetaUtil.java b/hutool-db/src/main/java/cn/hutool/db/meta/MetaUtil.java new file mode 100644 index 000000000..4d0f0d762 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/meta/MetaUtil.java @@ -0,0 +1,255 @@ +package cn.hutool.db.meta; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import javax.sql.DataSource; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.util.StrUtil; +import cn.hutool.db.DbRuntimeException; +import cn.hutool.db.DbUtil; +import cn.hutool.db.Entity; + +/** + * 数据库元数据信息工具类 + * + * @author looly + * + */ +public class MetaUtil { + /** + * 获得所有表名 + * + * @param ds 数据源 + * @return 表名列表 + */ + public static List getTables(DataSource ds) { + return getTables(ds, TableType.TABLE); + } + + /** + * 获得所有表名 + * + * @param ds 数据源 + * @param types 表类型 + * @return 表名列表 + */ + public static List getTables(DataSource ds, TableType... types) { + return getTables(ds, null, null, types); + } + + /** + * 获得所有表名 + * + * @param ds 数据源 + * @param schema 表数据库名,对于Oracle为用户名 + * @param types 表类型 + * @return 表名列表 + * @since 3.3.1 + */ + public static List getTables(DataSource ds, String schema, TableType... types) { + return getTables(ds, schema, null, types); + } + + /** + * 获得所有表名 + * + * @param ds 数据源 + * @param schema 表数据库名,对于Oracle为用户名 + * @param tableName 表名 + * @param types 表类型 + * @return 表名列表 + * @since 3.3.1 + */ + public static List getTables(DataSource ds, String schema, String tableName, TableType... types) { + final List tables = new ArrayList(); + Connection conn = null; + ResultSet rs = null; + try { + conn = ds.getConnection(); + + // catalog和schema获取失败默认使用null代替 + String catalog = getCataLog(conn); + if(null == schema) { + schema = getSchema(conn); + } + + final DatabaseMetaData metaData = conn.getMetaData(); + rs = metaData.getTables(catalog, schema, tableName, Convert.toStrArray(types)); + if (rs == null) { + return null; + } + String table; + while (rs.next()) { + table = rs.getString("TABLE_NAME"); + if (StrUtil.isNotBlank(table)) { + tables.add(table); + } + } + } catch (Exception e) { + throw new DbRuntimeException("Get tables error!", e); + } finally { + DbUtil.close(rs, conn); + } + return tables; + } + + /** + * 获得结果集的所有列名 + * + * @param rs 结果集 + * @return 列名数组 + * @throws DbRuntimeException SQL执行异常 + */ + public static String[] getColumnNames(ResultSet rs) throws DbRuntimeException { + try { + ResultSetMetaData rsmd = rs.getMetaData(); + int columnCount = rsmd.getColumnCount(); + String[] labelNames = new String[columnCount]; + for (int i = 0; i < labelNames.length; i++) { + labelNames[i] = rsmd.getColumnLabel(i + 1); + } + return labelNames; + } catch (Exception e) { + throw new DbRuntimeException("Get colunms error!", e); + } + } + + /** + * 获得表的所有列名 + * + * @param ds 数据源 + * @param tableName 表名 + * @return 列数组 + * @throws DbRuntimeException SQL执行异常 + */ + public static String[] getColumnNames(DataSource ds, String tableName) { + List columnNames = new ArrayList(); + Connection conn = null; + ResultSet rs = null; + try { + conn = ds.getConnection(); + + // catalog和schema获取失败默认使用null代替 + String catalog = getCataLog(conn); + String schema = getSchema(conn); + + final DatabaseMetaData metaData = conn.getMetaData(); + rs = metaData.getColumns(catalog, schema, tableName, null); + while (rs.next()) { + columnNames.add(rs.getString("COLUMN_NAME")); + } + return columnNames.toArray(new String[columnNames.size()]); + } catch (Exception e) { + throw new DbRuntimeException("Get columns error!", e); + } finally { + DbUtil.close(rs, conn); + } + } + + /** + * 创建带有字段限制的Entity对象
+ * 此方法读取数据库中对应表的字段列表,加入到Entity中,当Entity被设置内容时,会忽略对应表字段外的所有KEY + * + * @param ds 数据源 + * @param tableName 表名 + * @return Entity对象 + */ + public static Entity createLimitedEntity(DataSource ds, String tableName) { + final String[] columnNames = getColumnNames(ds, tableName); + return Entity.create(tableName).setFieldNames(columnNames); + } + + /** + * 获得表的元信息 + * + * @param ds 数据源 + * @param tableName 表名 + * @return Table对象 + */ + @SuppressWarnings("resource") + public static Table getTableMeta(DataSource ds, String tableName) { + final Table table = Table.create(tableName); + Connection conn = null; + ResultSet rs = null; + try { + conn = ds.getConnection(); + + // catalog和schema获取失败默认使用null代替 + String catalog = getCataLog(conn); + String schema = getSchema(conn); + + final DatabaseMetaData metaData = conn.getMetaData(); + + // 获得表元数据(表注释) + rs = metaData.getTables(catalog, schema, tableName, new String[] { TableType.TABLE.value() }); + if (rs.next()) { + table.setComment(rs.getString("REMARKS")); + } + + // 获得主键 + rs = metaData.getPrimaryKeys(catalog, schema, tableName); + while (rs.next()) { + table.addPk(rs.getString("COLUMN_NAME")); + } + + // 获得列 + rs = metaData.getColumns(catalog, schema, tableName, null); + while (rs.next()) { + table.setColumn(Column.create(tableName, rs)); + } + } catch (SQLException e) { + throw new DbRuntimeException("Get columns error!", e); + } finally { + DbUtil.close(rs, conn); + } + + return table; + } + + /** + * 获取catalog,获取失败返回{@code null} + * + * @param conn {@link Connection} 数据库连接,{@code null}时返回null + * @return catalog,获取失败返回{@code null} + * @since 4.6.0 + */ + public static String getCataLog(Connection conn) { + if (null == conn) { + return null; + } + try { + return conn.getCatalog(); + } catch (SQLException e) { + // ignore + } + + return null; + } + + /** + * 获取schema,获取失败返回{@code null} + * + * @param conn {@link Connection} 数据库连接,{@code null}时返回null + * @return schema,获取失败返回{@code null} + * @since 4.6.0 + */ + public static String getSchema(Connection conn) { + if (null == conn) { + return null; + } + try { + return conn.getSchema(); + } catch (SQLException e) { + // ignore + } + + return null; + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/meta/Table.java b/hutool-db/src/main/java/cn/hutool/db/meta/Table.java new file mode 100644 index 000000000..b5e5f4098 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/meta/Table.java @@ -0,0 +1,142 @@ +package cn.hutool.db.meta; + +import java.io.Serializable; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +/** + * 数据库表信息 + * + * @author loolly + * + */ +public class Table implements Serializable, Cloneable { + private static final long serialVersionUID = -810699625961392983L; + + /** 表名 */ + private String tableName; + /** 注释 */ + private String comment; + /** 主键字段名列表 */ + private Set pkNames = new LinkedHashSet(); + private Map columns = new LinkedHashMap<>(); + + public static Table create(String tableName) { + return new Table(tableName); + } + + // ----------------------------------------------------- Constructor start + /** + * 构造 + * + * @param tableName 表名 + */ + public Table(String tableName) { + this.setTableName(tableName); + } + // ----------------------------------------------------- Constructor end + + // ----------------------------------------------------- Getters and Setters start + /** + * 获取表名 + * + * @return 表名 + */ + public String getTableName() { + return tableName; + } + + /** + * 设置表名 + * + * @param tableName 表名 + */ + public void setTableName(String tableName) { + this.tableName = tableName; + } + + /** + * 获取注释 + * + * @return 注释 + */ + public String getComment() { + return comment; + } + + /** + * 设置注释 + * + * @param comment 注释 + * @return this + */ + public Table setComment(String comment) { + this.comment = comment; + return this; + } + + /** + * 获取主键列表 + * + * @return 主键列表 + */ + public Set getPkNames() { + return pkNames; + } + + /** + * 设置主键列表 + * + * @param pkNames 主键列表 + */ + public void setPkNames(Set pkNames) { + this.pkNames = pkNames; + } + // ----------------------------------------------------- Getters and Setters end + + /** + * 设置列对象 + * + * @param column 列对象 + * @return 自己 + */ + public Table setColumn(Column column) { + this.columns.put(column.getName(), column); + return this; + } + + /** + * 获取某列信息 + * + * @param name 列名 + * @return 列对象 + * @since 4.2.2 + */ + public Column getColumn(String name) { + return this.columns.get(name); + } + + /** + * 获取所有字段元信息 + * + * @return 字段元信息集合 + * @since 4.5.8 + */ + public Collection getColumns() { + return this.columns.values(); + } + + /** + * 添加主键 + * + * @param pkColumnName 主键的列名 + * @return 自己 + */ + public Table addPk(String pkColumnName) { + this.pkNames.add(pkColumnName); + return this; + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/meta/TableType.java b/hutool-db/src/main/java/cn/hutool/db/meta/TableType.java new file mode 100644 index 000000000..84d231325 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/meta/TableType.java @@ -0,0 +1,38 @@ +package cn.hutool.db.meta; + +/** + * 元信息中表的类型 + * @author Looly + * + */ +public enum TableType { + TABLE("TABLE"), + VIEW("VIEW"), + SYSTEM_TABLE ("SYSTEM TABLE"), + GLOBAL_TEMPORARY("GLOBAL TEMPORARY"), + LOCAL_TEMPORARY("LOCAL TEMPORARY"), + ALIAS("ALIAS"), + SYNONYM("SYNONYM"); + + private String value; + + /** + * 构造 + * @param value 值 + */ + TableType(String value){ + this.value = value; + } + /** + * 获取值 + * @return 值 + */ + public String value(){ + return this.value; + } + + @Override + public String toString() { + return this.value(); + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/meta/package-info.java b/hutool-db/src/main/java/cn/hutool/db/meta/package-info.java new file mode 100644 index 000000000..e4e1ff0d5 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/meta/package-info.java @@ -0,0 +1,7 @@ +/** + * JDBC数据表元数据信息封装,包括表结构、列信息的封装,入口为MetaUtil + * + * @author looly + * + */ +package cn.hutool.db.meta; \ No newline at end of file diff --git a/hutool-db/src/main/java/cn/hutool/db/nosql/mongo/MongoDS.java b/hutool-db/src/main/java/cn/hutool/db/nosql/mongo/MongoDS.java new file mode 100644 index 000000000..5180d6570 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/nosql/mongo/MongoDS.java @@ -0,0 +1,405 @@ +package cn.hutool.db.nosql.mongo; + +import java.io.Closeable; +import java.util.ArrayList; +import java.util.List; + +import org.bson.Document; + +import com.mongodb.MongoClient; +import com.mongodb.MongoClientOptions; +import com.mongodb.MongoClientOptions.Builder; +import com.mongodb.MongoCredential; +import com.mongodb.ServerAddress; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; + +import cn.hutool.core.exceptions.NotInitedException; +import cn.hutool.core.net.NetUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.db.DbRuntimeException; +import cn.hutool.log.Log; +import cn.hutool.log.StaticLog; +import cn.hutool.setting.Setting; + +/** + * MongoDB工具类 + * + * @author xiaoleilu + * + */ +public class MongoDS implements Closeable { + private final static Log log = StaticLog.get(); + + /** 默认配置文件 */ + public final static String MONGO_CONFIG_PATH = "config/mongo.setting"; + + // MongoDB配置文件 + private Setting setting; + // MongoDB实例连接列表 + private String[] groups; + // MongoDB单点连接信息 + private ServerAddress serverAddress; + // MongoDB客户端对象 + private MongoClient mongo; + + // --------------------------------------------------------------------------- Constructor start + /** + * 构造MongoDB数据源
+ * 调用者必须持有MongoDS实例,否则会被垃圾回收导致写入失败! + * + * @param host 主机(域名或者IP) + * @param port 端口 + */ + public MongoDS(String host, int port) { + this.serverAddress = createServerAddress(host, port); + initSingle(); + } + + /** + * 构造MongoDB数据源
+ * 调用者必须持有MongoDS实例,否则会被垃圾回收导致写入失败! + * + * @param mongoSetting MongoDB的配置文件,如果是null则读取默认配置文件或者使用MongoDB默认客户端配置 + * @param host 主机(域名或者IP) + * @param port 端口 + */ + public MongoDS(Setting mongoSetting, String host, int port) { + this.setting = mongoSetting; + this.serverAddress = createServerAddress(host, port); + initSingle(); + } + + /** + * 构造MongoDB数据源
+ * 当提供多个数据源时,这些数据源将为一个副本集或者多个mongos
+ * 调用者必须持有MongoDS实例,否则会被垃圾回收导致写入失败! 官方文档: http://docs.mongodb.org/manual/administration/replica-sets/ + * + * @param groups 分组列表,当为null或空时使用无分组配置,一个分组使用单一模式,否则使用副本集模式 + */ + public MongoDS(String... groups) { + this.groups = groups; + init(); + } + + /** + * 构造MongoDB数据源
+ * 当提供多个数据源时,这些数据源将为一个副本集或者mongos
+ * 调用者必须持有MongoDS实例,否则会被垃圾回收导致写入失败!
+ * 官方文档: http://docs.mongodb.org/manual/administration/replica-sets/ + * + * @param mongoSetting MongoDB的配置文件,必须有 + * @param groups 分组列表,当为null或空时使用无分组配置,一个分组使用单一模式,否则使用副本集模式 + */ + public MongoDS(Setting mongoSetting, String... groups) { + if (mongoSetting == null) { + throw new DbRuntimeException("Mongo setting is null!"); + } + this.setting = mongoSetting; + this.groups = groups; + init(); + } + // --------------------------------------------------------------------------- Constructor end + + /** + * 初始化,当给定分组数大于一个时使用 + */ + public void init() { + if (groups != null && groups.length > 1) { + initCloud(); + } else { + initSingle(); + } + } + + /** + * 初始化
+ * 设定文件中的host和端口有三种形式: + * + *
+	 * host = host:port
+	 * 
+ * + *
+	 * host = host
+	 * port = port
+	 * 
+ * + *
+	 * host = host
+	 * 
+ */ + synchronized public void initSingle() { + if (setting == null) { + try { + setting = new Setting(MONGO_CONFIG_PATH, true); + } catch (Exception e) { + // 在single模式下,可以没有配置文件。 + } + } + + String group = StrUtil.EMPTY; + if (null == this.serverAddress) { + //存在唯一分组 + if (groups != null && groups.length == 1) { + group = groups[0]; + } + serverAddress = createServerAddress(group); + } + + final MongoCredential credentail = createCredentail(group); + try { + if (null == credentail) { + mongo = new MongoClient(serverAddress, buildMongoClientOptions(group)); + } else { + mongo = new MongoClient(serverAddress, credentail, buildMongoClientOptions(group)); + } + } catch (Exception e) { + throw new DbRuntimeException(StrUtil.format("Init MongoDB pool with connection to [{}] error!", serverAddress), e); + } + + log.info("Init MongoDB pool with connection to [{}]", serverAddress); + } + + /** + * 初始化集群
+ * 集群的其它客户端设定参数使用全局设定
+ * 集群中每一个实例成员用一个group表示,例如: + * + *
+	 * user = test1
+	 * pass = 123456
+	 * database = test
+	 * [db0]
+	 * host = 192.168.1.1:27117 
+	 * [db1]
+	 * host = 192.168.1.1:27118 
+	 * [db2]
+	 * host = 192.168.1.1:27119
+	 * 
+ */ + synchronized public void initCloud() { + if (groups == null || groups.length == 0) { + throw new DbRuntimeException("Please give replication set groups!"); + } + + if (setting == null) { + // 若未指定配置文件,则使用默认配置文件 + setting = new Setting(MONGO_CONFIG_PATH, true); + } + + final List addrList = new ArrayList(); + for (String group : groups) { + addrList.add(createServerAddress(group)); + } + + final MongoCredential credentail = createCredentail(StrUtil.EMPTY); + try { + if (null == credentail) { + mongo = new MongoClient(addrList, buildMongoClientOptions(StrUtil.EMPTY)); + } else { + mongo = new MongoClient(addrList, credentail, buildMongoClientOptions(StrUtil.EMPTY)); + } + } catch (Exception e) { + log.error(e, "Init MongoDB connection error!"); + return; + } + + log.info("Init MongoDB cloud Set pool with connection to {}", addrList); + } + + /** + * 设定MongoDB配置文件 + * + * @param setting 配置文件 + */ + public void setSetting(Setting setting) { + this.setting = setting; + } + + /** + * @return 获得MongoDB客户端对象 + */ + public MongoClient getMongo() { + return mongo; + } + + /** + * 获得DB + * + * @param dbName DB + * @return DB + */ + public MongoDatabase getDb(String dbName) { + return mongo.getDatabase(dbName); + } + + /** + * 获得MongoDB中指定集合对象 + * + * @param dbName 库名 + * @param collectionName 集合名 + * @return DBCollection + */ + public MongoCollection getCollection(String dbName, String collectionName) { + return getDb(dbName).getCollection(collectionName); + } + + @Override + public void close() { + mongo.close(); + } + + // --------------------------------------------------------------------------- Private method start + /** + * 创建ServerAddress对象,会读取配置文件中的相关信息 + * + * @param group 分组,如果为null默认为无分组 + * @return ServerAddress + */ + private ServerAddress createServerAddress(String group) { + final Setting setting = checkSetting(); + + if (group == null) { + group = StrUtil.EMPTY; + } + + final String tmpHost = setting.getByGroup("host", group); + if (StrUtil.isBlank(tmpHost)) { + throw new NotInitedException("Host name is empy of group: {}", group); + } + + final int defaultPort = setting.getInt("port", group, 27017); + return new ServerAddress(NetUtil.buildInetSocketAddress(tmpHost, defaultPort)); + } + + /** + * 创建ServerAddress对象 + * + * @param host 主机域名或者IP(如果为空默认127.0.0.1) + * @param port 端口(如果为空默认为) + * @return ServerAddress + */ + private ServerAddress createServerAddress(String host, int port) { + return new ServerAddress(host, port); + } + + /** + * 创建{@link MongoCredential},用于服务端验证
+ * 此方法会首先读取指定分组下的属性,用户没有定义则读取空分组下的属性 + * + * @param group 分组 + * @return {@link MongoCredential},如果用户未指定用户名密码返回null + * @since 4.1.20 + */ + private MongoCredential createCredentail(String group) { + final Setting setting = this.setting; + if(null == setting) { + return null; + } + final String user = setting.getStr("user", group, setting.getStr("user")); + final String pass = setting.getStr("pass", group, setting.getStr("pass")); + final String database = setting.getStr("database", group, setting.getStr("database")); + return createCredentail(user, database, pass); + } + + /** + * 创建{@link MongoCredential},用于服务端验证 + * + * @param userName 用户名 + * @param database 数据库名 + * @param password 密码 + * @return {@link MongoCredential} + * @since 4.1.20 + */ + private MongoCredential createCredentail(String userName, String database, String password) { + if (StrUtil.hasEmpty(userName, database, database)) { + return null; + } + return MongoCredential.createCredential(userName, database, password.toCharArray()); + } + + /** + * 构件MongoDB连接选项
+ * + * @param group 分组,当分组对应的选项不存在时会读取根选项,如果也不存在使用默认值 + * @return MongoClientOptions + */ + private MongoClientOptions buildMongoClientOptions(String group) { + return buildMongoClientOptions(MongoClientOptions.builder(), group).build(); + } + + /** + * 构件MongoDB连接选项
+ * + * @param group 分组,当分组对应的选项不存在时会读取根选项,如果也不存在使用默认值 + * @return Builder + */ + private Builder buildMongoClientOptions(Builder builder, String group) { + if (setting == null) { + return builder; + } + + if (group == null) { + group = StrUtil.EMPTY; + } else { + group = group + StrUtil.DOT; + } + + // 每个主机答应的连接数(每个主机的连接池大小),当连接池被用光时,会被阻塞住 + Integer connectionsPerHost = setting.getInt(group + "connectionsPerHost"); + if (StrUtil.isBlank(group) == false && connectionsPerHost == null) { + connectionsPerHost = setting.getInt("connectionsPerHost"); + } + if (connectionsPerHost != null) { + builder.connectionsPerHost(connectionsPerHost); + log.debug("MongoDB connectionsPerHost: {}", connectionsPerHost); + } + + // multiplier for connectionsPerHost for # of threads that can block if connectionsPerHost is 10, and threadsAllowedToBlockForConnectionMultiplier is 5, then 50 threads can block more than + // that and an exception will be throw --int + Integer threadsAllowedToBlockForConnectionMultiplier = setting.getInt(group + "threadsAllowedToBlockForConnectionMultiplier"); + if (StrUtil.isBlank(group) == false && threadsAllowedToBlockForConnectionMultiplier == null) { + threadsAllowedToBlockForConnectionMultiplier = setting.getInt("threadsAllowedToBlockForConnectionMultiplier"); + } + if (threadsAllowedToBlockForConnectionMultiplier != null) { + builder.threadsAllowedToBlockForConnectionMultiplier(threadsAllowedToBlockForConnectionMultiplier); + log.debug("MongoDB threadsAllowedToBlockForConnectionMultiplier: {}", threadsAllowedToBlockForConnectionMultiplier); + } + + // 被阻塞线程从连接池获取连接的最长等待时间(ms) --int + Integer connectTimeout = setting.getInt(group + "connectTimeout"); + if (StrUtil.isBlank(group) == false && connectTimeout == null) { + setting.getInt("connectTimeout"); + } + if (connectTimeout != null) { + builder.connectTimeout(connectTimeout); + log.debug("MongoDB connectTimeout: {}", connectTimeout); + } + + // 套接字超时时间;该值会被传递给Socket.setSoTimeout(int)。默以为0(无穷) --int + Integer socketTimeout = setting.getInt(group + "socketTimeout"); + if (StrUtil.isBlank(group) == false && socketTimeout == null) { + setting.getInt("socketTimeout"); + } + if (socketTimeout != null) { + builder.socketTimeout(socketTimeout); + log.debug("MongoDB socketTimeout: {}", socketTimeout); + } + + return builder; + } + + /** + * 检查Setting配置文件 + * + * @return Setting配置文件 + */ + private Setting checkSetting() { + if (null == this.setting) { + throw new DbRuntimeException("Please indicate setting file or create default [{}]", MONGO_CONFIG_PATH); + } + return this.setting; + } + // --------------------------------------------------------------------------- Private method end +} \ No newline at end of file diff --git a/hutool-db/src/main/java/cn/hutool/db/nosql/mongo/MongoFactory.java b/hutool-db/src/main/java/cn/hutool/db/nosql/mongo/MongoFactory.java new file mode 100644 index 000000000..b616a866f --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/nosql/mongo/MongoFactory.java @@ -0,0 +1,125 @@ +package cn.hutool.db.nosql.mongo; + +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.setting.Setting; + +/** + * MongoDB工厂类,用于创建 + * @author looly + * + */ +public class MongoFactory { + + /** 各分组做组合key的时候分隔符 */ + private final static String GROUP_SEPRATER = ","; + + /** 数据源池 */ + private static Map dsMap = new ConcurrentHashMap<>(); + + // JVM关闭前关闭MongoDB连接 + static { + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + MongoFactory.closeAll(); + } + }); + } + + // ------------------------------------------------------------------------ Get DS start + /** + * 获取MongoDB数据源
+ * + * @param host 主机 + * @param port 端口 + * @return MongoDB连接 + */ + public static MongoDS getDS(String host, int port) { + final String key = host + ":" + port; + MongoDS ds = dsMap.get(key); + if (null == ds) { + // 没有在池中加入之 + ds = new MongoDS(host, port); + dsMap.put(key, ds); + } + + return ds; + } + + /** + * 获取MongoDB数据源
+ * 多个分组名对应的连接组成集群 + * + * @param groups 分组列表 + * @return MongoDB连接 + */ + public static MongoDS getDS(String... groups) { + final String key = ArrayUtil.join(groups, GROUP_SEPRATER); + MongoDS ds = dsMap.get(key); + if (null == ds) { + // 没有在池中加入之 + ds = new MongoDS(groups); + dsMap.put(key, ds); + } + + return ds; + } + + /** + * 获取MongoDB数据源
+ * + * @param groups 分组列表 + * @return MongoDB连接 + */ + public static MongoDS getDS(Collection groups) { + return getDS(groups.toArray(new String[groups.size()])); + } + + /** + * 获取MongoDB数据源
+ * + * @param setting 设定文件 + * @param groups 分组列表 + * @return MongoDB连接 + */ + public static MongoDS getDS(Setting setting, String... groups) { + final String key = setting.getSettingPath() + GROUP_SEPRATER + ArrayUtil.join(groups, GROUP_SEPRATER); + MongoDS ds = dsMap.get(key); + if (null == ds) { + // 没有在池中加入之 + ds = new MongoDS(setting, groups); + dsMap.put(key, ds); + } + + return ds; + } + + /** + * 获取MongoDB数据源
+ * + * @param setting 配置文件 + * @param groups 分组列表 + * @return MongoDB连接 + */ + public static MongoDS getDS(Setting setting, Collection groups) { + return getDS(setting, groups.toArray(new String[groups.size()])); + } + // ------------------------------------------------------------------------ Get DS ends + + /** + * 关闭全部连接 + */ + public static void closeAll() { + if(CollectionUtil.isNotEmpty(dsMap)){ + for(MongoDS ds : dsMap.values()) { + ds.close(); + } + dsMap.clear(); + } + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/nosql/mongo/package-info.java b/hutool-db/src/main/java/cn/hutool/db/nosql/mongo/package-info.java new file mode 100644 index 000000000..8240bfdaa --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/nosql/mongo/package-info.java @@ -0,0 +1,7 @@ +/** + * MongoDB数据库操作的封装 + * + * @author looly + * + */ +package cn.hutool.db.nosql.mongo; \ No newline at end of file diff --git a/hutool-db/src/main/java/cn/hutool/db/nosql/package-info.java b/hutool-db/src/main/java/cn/hutool/db/nosql/package-info.java new file mode 100644 index 000000000..6298aad69 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/nosql/package-info.java @@ -0,0 +1,7 @@ +/** + * NoSQL封装,包括Redis和MongoDB等数据库操作的封装 + * + * @author looly + * + */ +package cn.hutool.db.nosql; \ No newline at end of file diff --git a/hutool-db/src/main/java/cn/hutool/db/nosql/redis/RedisDS.java b/hutool-db/src/main/java/cn/hutool/db/nosql/redis/RedisDS.java new file mode 100644 index 000000000..0f1779243 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/nosql/redis/RedisDS.java @@ -0,0 +1,180 @@ +package cn.hutool.db.nosql.redis; + +import java.io.Closeable; +import java.io.IOException; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.setting.Setting; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.Protocol; + +/** + * Jedis数据源 + * + * @author looly + * @since 3.2.3 + */ +public class RedisDS implements Closeable{ + /** 默认配置文件 */ + public final static String REDIS_CONFIG_PATH = "config/redis.setting"; + + /** 配置文件 */ + private Setting setting; + /** Jedis连接池 */ + private JedisPool pool; + + // --------------------------------------------------------------------------------- Static method start + /** + * 创建RedisDS,使用默认配置文件,默认分组 + * + * @return {@link RedisDS} + */ + public static RedisDS create() { + return new RedisDS(); + } + + /** + * 创建RedisDS,使用默认配置文件 + * + * @param group 配置文件中配置分组 + * @return {@link RedisDS} + */ + public static RedisDS create(String group) { + return new RedisDS(group); + } + + /** + * 创建RedisDS + * + * @param setting 配置文件 + * @param group 配置文件中配置分组 + * @return {@link RedisDS} + */ + public static RedisDS create(Setting setting, String group) { + return new RedisDS(setting, group); + } + // --------------------------------------------------------------------------------- Static method end + + /** + * 构造,使用默认配置文件,默认分组 + */ + public RedisDS() { + this(null, null); + } + + /** + * 构造,使用默认配置文件 + * + * @param group 配置文件中配置分组 + */ + public RedisDS(String group) { + this(null, group); + } + + /** + * 构造 + * + * @param setting 配置文件 + * @param group 配置文件中配置分组 + */ + public RedisDS(Setting setting, String group) { + this.setting = setting; + init(group); + } + + /** + * 初始化Jedis客户端 + * + * @param group Redis服务器信息分组 + * @return this + */ + public RedisDS init(String group) { + if (null == setting) { + setting = new Setting(REDIS_CONFIG_PATH, true); + } + + final JedisPoolConfig config = new JedisPoolConfig(); + // 共用配置 + setting.toBean(config); + if (StrUtil.isNotBlank(group)) { + // 特有配置 + setting.toBean(group, config); + } + + this.pool = new JedisPool(config, + // 地址 + setting.getStr("host", group, Protocol.DEFAULT_HOST), + // 端口 + setting.getInt("port", group, Protocol.DEFAULT_PORT), + // 连接超时 + setting.getInt("connectionTimeout", group, setting.getInt("timeout", group, Protocol.DEFAULT_TIMEOUT)), + // 读取数据超时 + setting.getInt("soTimeout", group, setting.getInt("timeout", group, Protocol.DEFAULT_TIMEOUT)), + // 密码 + setting.getStr("password", group, null), + // 数据库序号 + setting.getInt("database", group, Protocol.DEFAULT_DATABASE), + // 客户端名 + setting.getStr("clientName", group, "Hutool"), + // 是否使用SSL + setting.getBool("ssl", group, false), + // SSL相关,使用默认 + null, null, null); + + return this; + } + + /** + * 从资源池中获取{@link Jedis} + * + * @return {@link Jedis} + */ + public Jedis getJedis() { + return this.pool.getResource(); + } + + /** + * 从Redis中获取值 + * + * @param key 键 + * @return 值 + */ + public String getStr(String key) { + try (Jedis jedis = getJedis()) { + return jedis.get(key); + } + } + + /** + * 从Redis中获取值 + * + * @param key 键 + * @param value 值 + * @return 状态码 + */ + public String setStr(String key, String value) { + try (Jedis jedis = getJedis()) { + return jedis.set(key, value); + } + } + + /** + * 从Redis中删除多个值 + * + * @param keys 需要删除值对应的键列表 + * @return 删除个数,0表示无key可删除 + */ + public Long del(String... keys) { + try (Jedis jedis = getJedis()) { + return jedis.del(keys); + } + } + + @Override + public void close() throws IOException { + IoUtil.close(pool); + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/nosql/redis/package-info.java b/hutool-db/src/main/java/cn/hutool/db/nosql/redis/package-info.java new file mode 100644 index 000000000..7da112edc --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/nosql/redis/package-info.java @@ -0,0 +1,7 @@ +/** + * Redis(Jedis)数据库操作的封装 + * + * @author looly + * + */ +package cn.hutool.db.nosql.redis; \ No newline at end of file diff --git a/hutool-db/src/main/java/cn/hutool/db/package-info.java b/hutool-db/src/main/java/cn/hutool/db/package-info.java new file mode 100644 index 000000000..5663378b3 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/package-info.java @@ -0,0 +1,8 @@ +/** + * Hutool-db是一个在JDBC基础上封装的数据库操作工具类,通过包装,使用ActiveRecord思想操作数据库。
+ * 在Hutool-db中,使用Entity(本质上是个Map)代替Bean来使数据库操作更加灵活,同时提供Bean和Entity的转换提供传统ORM的兼容支持。 + * + * @author looly + * + */ +package cn.hutool.db; \ No newline at end of file diff --git a/hutool-db/src/main/java/cn/hutool/db/sql/Condition.java b/hutool-db/src/main/java/cn/hutool/db/sql/Condition.java new file mode 100644 index 000000000..ac47b4658 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/sql/Condition.java @@ -0,0 +1,493 @@ +package cn.hutool.db.sql; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import cn.hutool.core.clone.CloneSupport; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.text.StrSpliter; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 条件对象
+ * + * @author Looly + * + */ +public class Condition extends CloneSupport { + + /** + * SQL中 LIKE 语句查询方式
+ * + * @author Looly + * + */ + public static enum LikeType { + /** 以给定值开头,拼接后的SQL "value%" */ + StartWith, + /** 以给定值开头,拼接后的SQL "%value" */ + EndWith, + /** 包含给定值,拼接后的SQL "%value%" */ + Contains + } + + private static final String OPERATOR_LIKE = "LIKE"; + private static final String OPERATOR_IN = "IN"; + private static final String OPERATOR_IS = "IS"; + private static final String OPERATOR_IS_NOT = "IS NOT"; + private static final String OPERATOR_BETWEEN = "BETWEEN"; + private static final List OPERATORS = Arrays.asList("<>", "<=", "<", ">=", ">", "=", "!=", OPERATOR_IN); + + private static final String VALUE_NULL = "NULL"; + + /** 字段 */ + private String field; + /** 运算符(大于号,小于号,等于号 like 等) */ + private String operator; + /** 值 */ + private Object value; + /** 是否使用条件值占位符 */ + private boolean isPlaceHolder = true; + /** between firstValue and secondValue */ + private Object secondValue; + + /** + * 解析为Condition + * + * @param field 字段名 + * @param expression 表达式或普通值 + * @return Condition + */ + public static Condition parse(String field, Object expression) { + return new Condition(field, expression); + } + + // --------------------------------------------------------------- Constructor start + /** + * 构造 + */ + public Condition() { + } + + /** + * 构造 + * + * @param isPlaceHolder 是否使用条件值占位符 + */ + public Condition(boolean isPlaceHolder) { + this.isPlaceHolder = isPlaceHolder; + } + + /** + * 构造,使用等于表达式(运算符是=) + * + * @param field 字段 + * @param value 值 + */ + public Condition(String field, Object value) { + this(field, "=", value); + parseValue(); + } + + /** + * 构造 + * + * @param field 字段 + * @param operator 运算符(大于号,小于号,等于号 like 等) + * @param value 值 + */ + public Condition(String field, String operator, Object value) { + this.field = field; + this.operator = operator; + this.value = value; + } + + /** + * 构造 + * + * @param field 字段 + * @param value 值 + * @param likeType {@link LikeType} + */ + public Condition(String field, String value, LikeType likeType) { + this.field = field; + this.operator = OPERATOR_LIKE; + this.value = SqlUtil.buildLikeValue(value, likeType, false); + } + // --------------------------------------------------------------- Constructor start + + // --------------------------------------------------------------- Getters and Setters start + /** + * @return 字段 + */ + public String getField() { + return field; + } + + /** + * 设置字段名 + * + * @param field 字段名 + */ + public void setField(String field) { + this.field = field; + } + + /** + * 获得运算符
+ * 大于号,小于号,等于号 等 + * + * @return 运算符 + */ + public String getOperator() { + return operator; + } + + /** + * 设置运算符
+ * 大于号,小于号,等于号 等 + * + * @param operator 运算符 + */ + public void setOperator(String operator) { + this.operator = operator; + } + + /** + * 获得值 + * + * @return 值 + */ + public Object getValue() { + return value; + } + + /** + * 设置值,不解析表达式 + * + * @param value 值 + */ + public void setValue(Object value) { + setValue(value, false); + } + + /** + * 设置值 + * + * @param value 值 + * @param isParse 是否解析值表达式 + */ + public void setValue(Object value, boolean isParse) { + this.value = value; + if (isParse) { + parseValue(); + } + } + + /** + * 是否使用条件占位符 + * + * @return 是否使用条件占位符 + */ + public boolean isPlaceHolder() { + return isPlaceHolder; + } + + /** + * 设置是否使用条件占位符 + * + * @param isPlaceHolder 是否使用条件占位符 + */ + public void setPlaceHolder(boolean isPlaceHolder) { + this.isPlaceHolder = isPlaceHolder; + } + + /** + * 是否 between x and y 类型 + * + * @return 是否 between x and y 类型 + * @since 4.0.1 + */ + public boolean isOperatorBetween() { + return OPERATOR_BETWEEN.equalsIgnoreCase(this.operator); + } + + /** + * 是否IN条件 + * + * @return 是否IN条件 + * @since 4.0.1 + */ + public boolean isOperatorIn() { + return OPERATOR_IN.equalsIgnoreCase(this.operator); + } + + /** + * 是否IS条件 + * + * @return 是否IS条件 + * @since 4.0.1 + */ + public boolean isOperatorIs() { + return OPERATOR_IS.equalsIgnoreCase(this.operator); + } + + /** + * 检查值是否为null,如果为null转换为 "IS NULL"形式 + * + * @return this + */ + public Condition checkValueNull() { + if (null == this.value) { + this.operator = OPERATOR_IS; + this.value = VALUE_NULL; + } + return this; + } + + /** + * 获得between 类型中第二个值 + * + * @return 值 + */ + public Object getSecondValue() { + return secondValue; + } + + /** + * 设置between 类型中第二个值 + * + * @param secondValue 第二个值 + */ + public void setSecondValue(Object secondValue) { + this.secondValue = secondValue; + } + + // --------------------------------------------------------------- Getters and Setters end + + @Override + public String toString() { + return toString(null); + } + + /** + * 转换为条件字符串,并回填占位符对应的参数值 + * + * @param paramValues 参数列表,用于回填占位符对应参数值 + * @return 条件字符串 + */ + public String toString(List paramValues) { + final StringBuilder conditionStrBuilder = StrUtil.builder(); + // 判空值 + checkValueNull(); + + // 固定前置,例如:"name ="、"name IN"、"name BETWEEN"、"name LIKE" + conditionStrBuilder.append(this.field).append(StrUtil.SPACE).append(this.operator); + + if (isOperatorBetween()) { + buildValuePartForBETWEEN(conditionStrBuilder, paramValues); + } else if (isOperatorIn()) { + // 类似:" (?,?,?)" 或者 " (1,2,3,4)" + buildValuePartForIN(conditionStrBuilder, paramValues); + } else { + if (isPlaceHolder() && false == isOperatorIs()) { + // 使用条件表达式占位符,条件表达式并不适用于 IS NULL + conditionStrBuilder.append(" ?"); + if(null != paramValues) { + paramValues.add(this.value); + } + } else { + // 直接使用条件值 + conditionStrBuilder.append(" ").append(this.value); + } + } + + return conditionStrBuilder.toString(); + } + + // ----------------------------------------------------------------------------------------------- Private method start + /** + * 构建BETWEEN语句中的值部分
+ * 开头必须加空格,类似:" ? AND ?" 或者 " 1 AND 2" + * + * @param conditionStrBuilder 条件语句构建器 + * @param paramValues 参数集合,用于参数占位符对应参数回填 + */ + private void buildValuePartForBETWEEN(StringBuilder conditionStrBuilder, List paramValues) { + // BETWEEN x AND y 的情况,两个参数 + if (isPlaceHolder()) { + // 使用条件表达式占位符 + conditionStrBuilder.append(" ?"); + if(null != paramValues) { + paramValues.add(this.value); + } + } else { + // 直接使用条件值 + conditionStrBuilder.append(CharUtil.SPACE).append(this.value); + } + + // 处理 AND y + conditionStrBuilder.append(StrUtil.SPACE).append(LogicalOperator.AND.toString()); + if (isPlaceHolder()) { + // 使用条件表达式占位符 + conditionStrBuilder.append(" ?"); + if(null != paramValues) { + paramValues.add(this.secondValue); + } + } else { + // 直接使用条件值 + conditionStrBuilder.append(CharUtil.SPACE).append(this.secondValue); + } + } + + /** + * 构建IN语句中的值部分
+ * 开头必须加空格,类似:" (?,?,?)" 或者 " (1,2,3,4)" + * + * @param conditionStrBuilder 条件语句构建器 + * @param paramValues 参数集合,用于参数占位符对应参数回填 + */ + private void buildValuePartForIN(StringBuilder conditionStrBuilder, List paramValues) { + conditionStrBuilder.append(" ("); + final Object value = this.value; + if (isPlaceHolder()) { + List valuesForIn; + // 占位符对应值列表 + if (value instanceof CharSequence) { + valuesForIn = StrUtil.split((CharSequence) value, ','); + } else { + valuesForIn = Arrays.asList(Convert.convert(String[].class, value)); + if (null == valuesForIn) { + valuesForIn = CollUtil.newArrayList(Convert.toStr(value)); + } + } + conditionStrBuilder.append(StrUtil.repeatAndJoin("?", valuesForIn.size(), ",")); + if(null != paramValues) { + paramValues.addAll(valuesForIn); + } + } else { + conditionStrBuilder.append(StrUtil.join(",", value)); + } + conditionStrBuilder.append(')'); + } + + /** + * 解析值表达式
+ * 支持"<>", "<=", "< ", ">=", "> ", "= ", "!=", "IN", "LIKE", "IS", "IS NOT"表达式
+ * 如果无法识别表达式,则表达式为"=",表达式与值用空格隔开
+ * 例如字段为name,那value可以为:"> 1"或者 "LIKE %Tom"此类 + */ + private void parseValue() { + // 当值无时,视为空判定 + if (null == this.value) { + this.operator = OPERATOR_IS; + this.value = VALUE_NULL; + return; + } + + // 对数组和集合值按照 IN 处理 + if (this.value instanceof Collection || ArrayUtil.isArray(this.value)) { + this.operator = OPERATOR_IN; + return; + } + + // 其他类型值,跳过 + if (false == (this.value instanceof String)) { + return; + } + + String valueStr = ((String) value); + if (StrUtil.isBlank(valueStr)) { + // 空字段不做处理 + return; + } + + valueStr = valueStr.trim(); + + // 处理null + if (StrUtil.endWithIgnoreCase(valueStr, "null")) { + if (StrUtil.equalsIgnoreCase("= null", valueStr) || StrUtil.equalsIgnoreCase("is null", valueStr)) { + // 处理"= null"和"is null"转换为"IS NULL" + this.operator = OPERATOR_IS; + this.value = VALUE_NULL; + this.isPlaceHolder = false; + return; + } else if (StrUtil.equalsIgnoreCase("!= null", valueStr) || StrUtil.equalsIgnoreCase("is not null", valueStr)) { + // 处理"!= null"和"is not null"转换为"IS NOT NULL" + this.operator = OPERATOR_IS_NOT; + this.value = VALUE_NULL; + this.isPlaceHolder = false; + return; + } + } + + List strs = StrUtil.split(valueStr, StrUtil.C_SPACE, 2); + if (strs.size() < 2) { + return; + } + + // 处理常用符号和IN + final String firstPart = strs.get(0).trim().toUpperCase(); + if (OPERATORS.contains(firstPart)) { + this.operator = firstPart; + this.value = strs.get(1).trim(); + return; + } + + // 处理LIKE + if (OPERATOR_LIKE.equals(firstPart)) { + this.operator = OPERATOR_LIKE; + this.value = unwrapQuote(strs.get(1)); + return; + } + + // 处理BETWEEN x AND y + if (OPERATOR_BETWEEN.equals(firstPart)) { + final List betweenValueStrs = StrSpliter.splitTrimIgnoreCase(strs.get(1), LogicalOperator.AND.toString(), 2, true); + if (betweenValueStrs.size() < 2) { + // 必须满足a AND b格式,不满足被当作普通值 + return; + } + + this.operator = OPERATOR_BETWEEN; + this.value = unwrapQuote(betweenValueStrs.get(0)); + this.secondValue = unwrapQuote(betweenValueStrs.get(1)); + return; + } + } + + /** + * 去掉包围在字符串两端的单引号或双引号 + * + * @param value 值 + * @return 去掉引号后的值 + */ + private static String unwrapQuote(String value) { + if (null == value) { + return null; + } + value = value.trim(); + + int from = 0; + int to = value.length(); + char startChar = value.charAt(0); + char endChar = value.charAt(to - 1); + if (startChar == endChar) { + if ('\'' == startChar || '"' == startChar) { + from = 1; + to = to - 1; + } + } + + if (from == 0 && to == value.length()) { + // 并不包含,返回原值 + return value; + } + return value.substring(from, to); + } + // ----------------------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-db/src/main/java/cn/hutool/db/sql/Direction.java b/hutool-db/src/main/java/cn/hutool/db/sql/Direction.java new file mode 100644 index 000000000..e0d841544 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/sql/Direction.java @@ -0,0 +1,32 @@ +package cn.hutool.db.sql; + +import cn.hutool.core.util.StrUtil; + +/** + * 排序方式(升序或者降序) + * @author Looly + * + */ +public enum Direction{ + /** 升序 */ + ASC, + /** 降序 */ + DESC; + + /** + * 根据字符串值返回对应{@link Direction}值 + * + * @param value 排序方式字符串,只能是 ASC或DESC + * @return {@link Direction} + * @throws IllegalArgumentException in case the given value cannot be parsed into an enum value. + */ + public static Direction fromString(String value) throws IllegalArgumentException{ + + try { + return Direction.valueOf(value.toUpperCase()); + } catch (Exception e) { + throw new IllegalArgumentException(StrUtil.format( + "Invalid value [{}] for orders given! Has to be either 'desc' or 'asc' (case insensitive).", value), e); + } + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/sql/LogicalOperator.java b/hutool-db/src/main/java/cn/hutool/db/sql/LogicalOperator.java new file mode 100644 index 000000000..429058969 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/sql/LogicalOperator.java @@ -0,0 +1,29 @@ +package cn.hutool.db.sql; + +import cn.hutool.core.util.StrUtil; + +/** + * 逻辑运算符 + * @author Looly + * + */ +public enum LogicalOperator{ + /** 且,两个条件都满足 */ + AND, + /** 或,满足多个条件的一个即可 */ + OR; + + /** + * 给定字符串逻辑运算符是否与当前逻辑运算符一致,不区分大小写,自动去除两边空白符 + * + * @param logicalOperatorStr 逻辑运算符字符串 + * @return 是否与当前逻辑运算符一致 + * @since 3.2.1 + */ + public boolean isSame(String logicalOperatorStr) { + if(StrUtil.isBlank(logicalOperatorStr)) { + return false; + } + return this.name().equalsIgnoreCase(logicalOperatorStr.trim()); + } +} \ No newline at end of file diff --git a/hutool-db/src/main/java/cn/hutool/db/sql/NamedSql.java b/hutool-db/src/main/java/cn/hutool/db/sql/NamedSql.java new file mode 100644 index 000000000..fa3a93d61 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/sql/NamedSql.java @@ -0,0 +1,135 @@ +package cn.hutool.db.sql; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import cn.hutool.core.text.StrBuilder; +import cn.hutool.core.util.StrUtil; + +/** + * 使用命名占位符的SQL,例如:select * from table where field1=:name1
+ * 支持的占位符格式为: + *
+ * 1、:name
+ * 2、?name
+ * 3、@name
+ * 
+ * + * @author looly + * @since 4.0.10 + */ +public class NamedSql { + + private String sql; + private List params; + + /** + * 构造 + * + * @param namedSql 命名占位符的SQL + * @param paramMap 名和参数的对应Map + */ + public NamedSql(String namedSql, Map paramMap) { + this.params = new LinkedList<>(); + parse(namedSql, paramMap); + } + + /** + * 获取SQL + * + * @return SQL + */ + public String getSql() { + return this.sql; + } + + /** + * 获取参数列表,按照占位符顺序 + * + * @return 参数数组 + */ + public Object[] getParams() { + return this.params.toArray(new Object[this.params.size()]); + } + + /** + * 获取参数列表,按照占位符顺序 + * + * @return 参数列表 + */ + public List getParamList() { + return this.params; + } + + /** + * 解析命名占位符的SQL + * + * @param namedSql 命名占位符的SQL + * @param paramMap 名和参数的对应Map + */ + private void parse(String namedSql, Map paramMap) { + int len = namedSql.length(); + + final StrBuilder name = StrUtil.strBuilder(); + final StrBuilder sqlBuilder = StrUtil.strBuilder(); + char c; + Character nameStartChar = null; + for (int i = 0; i < len; i++) { + c = namedSql.charAt(i); + if (c == ':' || c == '@' || c == '?') { + nameStartChar = c; + } else if (null != nameStartChar) { + // 变量状态 + if (isGenerateChar(c)) { + // 变量名 + name.append(c); + } else { + // 变量结束 + String nameStr = name.toString(); + if(paramMap.containsKey(nameStr)) { + // 有变量对应值(值可以为null),替换占位符 + final Object paramValue = paramMap.get(nameStr); + sqlBuilder.append('?'); + this.params.add(paramValue); + } else { + // 无变量对应值,原样输出 + sqlBuilder.append(nameStartChar).append(name); + } + nameStartChar = null; + name.clear(); + sqlBuilder.append(c); + } + } else { + sqlBuilder.append(c); + } + } + + if (false == name.isEmpty()) { + // SQL结束依旧有变量名存在,说明变量位于末尾 + final Object paramValue = paramMap.get(name.toString()); + if (null != paramValue) { + // 有变量对应值,替换占位符 + sqlBuilder.append('?'); + this.params.add(paramValue); + } else { + // 无变量对应值,原样输出 + sqlBuilder.append(nameStartChar).append(name); + } + nameStartChar = null; + name.clear(); + } + + this.sql = sqlBuilder.toString(); + } + + /** + * 是否为标准的字符,包括大小写字母、下划线和数字 + * + * @param c 字符 + * @return 是否标准字符 + */ + private static boolean isGenerateChar(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || (c >= '0' && c <= '9'); + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/sql/Order.java b/hutool-db/src/main/java/cn/hutool/db/sql/Order.java new file mode 100644 index 000000000..25b40258a --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/sql/Order.java @@ -0,0 +1,78 @@ +package cn.hutool.db.sql; + +import java.io.Serializable; + +import cn.hutool.core.util.StrUtil; + +/** + * SQL排序对象 + * @author Looly + * + */ +public class Order implements Serializable{ + private static final long serialVersionUID = 1L; + + /** 排序的字段 */ + private String field; + /** 排序方式(正序还是反序) */ + private Direction direction; + + //---------------------------------------------------------- Constructor start + public Order() { + } + + /** + * 构造 + * @param field 排序字段 + */ + public Order(String field) { + this.field = field; + } + + /** + * 构造 + * @param field 排序字段 + * @param direction 排序方式 + */ + public Order(String field, Direction direction) { + this(field); + this.direction = direction; + } + + //---------------------------------------------------------- Constructor end + + //---------------------------------------------------------- Getters and Setters start + /** + * @return 排序字段 + */ + public String getField() { + return this.field; + } + /** + * 设置排序字段 + * @param field 排序字段 + */ + public void setField(String field) { + this.field = field; + } + + /** + * @return 排序方向 + */ + public Direction getDirection() { + return direction; + } + /** + * 设置排序方向 + * @param direction 排序方向 + */ + public void setDirection(Direction direction) { + this.direction = direction; + } + //---------------------------------------------------------- Getters and Setters end + + @Override + public String toString() { + return StrUtil.builder().append(this.field).append(StrUtil.SPACE).append(null == direction ? StrUtil.EMPTY : direction).toString(); + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/sql/Query.java b/hutool-db/src/main/java/cn/hutool/db/sql/Query.java new file mode 100644 index 000000000..12adbe889 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/sql/Query.java @@ -0,0 +1,182 @@ +package cn.hutool.db.sql; + +import java.util.Collection; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.db.DbRuntimeException; +import cn.hutool.db.Page; + +/** + * 查询对象,用于传递查询所需的字段值
+ * 查询对象根据表名(可以多个),多个条件 {@link Condition} 构建查询对象完成查询。
+ * 如果想自定义返回结果,则可在查询对象中自定义要查询的字段名,分页{@link Page}信息来自定义结果。 + * + * @author Looly + * + */ +public class Query { + + /** 查询的字段名列表 */ + Collection fields; + /** 查询的表名 */ + String[] tableNames; + /** 查询的条件语句 */ + Condition[] where; + /** 分页对象 */ + Page page; + + // --------------------------------------------------------------- Constructor start + /** + * 构造 + * + * @param tableNames 表名 + */ + public Query(String... tableNames) { + this(null, tableNames); + this.tableNames = tableNames; + } + + /** + * 构造 + * + * @param where 条件语句 + * @param tableNames 表名 + */ + public Query(Condition[] where, String... tableNames) { + this(where, null, tableNames); + } + + /** + * 构造 + * + * @param where 条件语句 + * @param page 分页 + * @param tableNames 表名 + */ + public Query(Condition[] where, Page page, String... tableNames) { + this(null, tableNames, where, page); + } + + /** + * 构造 + * + * @param fields 字段 + * @param tableNames 表名 + * @param where 条件 + * @param page 分页 + */ + public Query(Collection fields, String[] tableNames, Condition[] where, Page page) { + this.fields = fields; + this.tableNames = tableNames; + this.where = where; + this.page = page; + } + // --------------------------------------------------------------- Constructor end + + // --------------------------------------------------------------- Getters and Setters start + /** + * 获得查询的字段名列表 + * + * @return 查询的字段名列表 + */ + public Collection getFields() { + return fields; + } + + /** + * 设置查询的字段名列表 + * + * @param fields 查询的字段名列表 + * @return this + */ + public Query setFields(Collection fields) { + this.fields = fields; + return this; + } + + /** + * 设置查询的字段名列表 + * + * @param fields 查询的字段名列表 + * @return this + */ + public Query setFields(String... fields) { + this.fields = CollectionUtil.newArrayList(fields); + return this; + } + + /** + * 获得表名数组 + * + * @return 表名数组 + */ + public String[] getTableNames() { + return tableNames; + } + + /** + * 设置表名 + * + * @param tableNames 表名 + * @return this + */ + public Query setTableNames(String... tableNames) { + this.tableNames = tableNames; + return this; + } + + /** + * 获得条件语句 + * + * @return 条件语句 + */ + public Condition[] getWhere() { + return where; + } + + /** + * 设置条件语句 + * + * @param where 条件语句 + * @return this + */ + public Query setWhere(Condition... where) { + this.where = where; + return this; + } + + /** + * 获得分页对象 + * + * @return 分页对象 + */ + public Page getPage() { + return page; + } + + /** + * 设置分页对象 + * + * @param page 分页对象 + * @return this + */ + public Query setPage(Page page) { + this.page = page; + return this; + } + // --------------------------------------------------------------- Getters and Setters end + + /** + * 获得第一个表名 + * + * @return 表名 + * @throws DbRuntimeException 没有表 + */ + public String getFirstTableName() throws DbRuntimeException { + if (ArrayUtil.isEmpty(this.tableNames)) { + throw new DbRuntimeException("No tableName!"); + } + return this.tableNames[0]; + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/sql/SqlBuilder.java b/hutool-db/src/main/java/cn/hutool/db/sql/SqlBuilder.java new file mode 100644 index 000000000..c5a9ef4ca --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/sql/SqlBuilder.java @@ -0,0 +1,632 @@ +package cn.hutool.db.sql; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map.Entry; + +import cn.hutool.core.builder.Builder; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.db.DbRuntimeException; +import cn.hutool.db.Entity; +import cn.hutool.db.dialect.DialectName; + +/** + * SQL构建器
+ * 首先拼接SQL语句,值使用 ? 占位
+ * 调用getParamValues()方法获得占位符对应的值 + * + * @author Looly + * + */ +public class SqlBuilder implements Builder{ + private static final long serialVersionUID = 1L; + + // --------------------------------------------------------------- Static methods start + /** + * 创建SQL构建器 + * + * @return SQL构建器 + */ + public static SqlBuilder create() { + return new SqlBuilder(); + } + + /** + * 创建SQL构建器 + * + * @param wrapper 包装器 + * @return SQL构建器 + */ + public static SqlBuilder create(Wrapper wrapper) { + return new SqlBuilder(wrapper); + } + + // --------------------------------------------------------------- Static methods end + + // --------------------------------------------------------------- Enums start + /** + * SQL中多表关联用的关键字 + * + * @author Looly + * + */ + public static enum Join { + /** 如果表中有至少一个匹配,则返回行 */ + INNER, + /** 即使右表中没有匹配,也从左表返回所有的行 */ + LEFT, + /** 即使左表中没有匹配,也从右表返回所有的行 */ + RIGHT, + /** 只要其中一个表中存在匹配,就返回行 */ + FULL + } + // --------------------------------------------------------------- Enums end + + final private StringBuilder sql = new StringBuilder(); + /** 字段列表(仅用于插入和更新) */ + final private List fields = new ArrayList(); + /** 占位符对应的值列表 */ + final private List paramValues = new ArrayList(); + /** 包装器 */ + private Wrapper wrapper; + + // --------------------------------------------------------------- Constructor start + public SqlBuilder() { + } + + public SqlBuilder(Wrapper wrapper) { + this.wrapper = wrapper; + } + // --------------------------------------------------------------- Constructor end + + // --------------------------------------------------------------- Builder start + + /** + * 插入,使用默认的ANSI方言 + * + * @param entity 实体 + * @return 自己 + */ + public SqlBuilder insert(Entity entity) { + return this.insert(entity, DialectName.ANSI); + } + + /** + * 插入
+ * 插入会忽略空的字段名及其对应值,但是对于有字段名对应值为{@code null}的情况不忽略 + * + * @param entity 实体 + * @param dialectName 方言名 + * @return 自己 + */ + public SqlBuilder insert(Entity entity, DialectName dialectName) { + // 验证 + validateEntity(entity); + + if (null != wrapper) { + // 包装表名 + // entity = wrapper.wrap(entity); + entity.setTableName(wrapper.wrap(entity.getTableName())); + } + + final boolean isOracle = ObjectUtil.equal(dialectName, DialectName.ORACLE);// 对Oracle的特殊处理 + final StringBuilder fieldsPart = new StringBuilder(); + final StringBuilder placeHolder = new StringBuilder(); + + boolean isFirst = true; + String field; + Object value; + for (Entry entry : entity.entrySet()) { + field = entry.getKey(); + value = entry.getValue(); + if (StrUtil.isNotBlank(field) /* && null != value */) { + if (isFirst) { + isFirst = false; + } else { + // 非第一个参数,追加逗号 + fieldsPart.append(", "); + placeHolder.append(", "); + } + + this.fields.add(field); + fieldsPart.append((null != wrapper) ? wrapper.wrap(field) : field); + if (isOracle && value instanceof String && StrUtil.endWithIgnoreCase((String) value, ".nextval")) { + // Oracle的特殊自增键,通过字段名.nextval获得下一个值 + placeHolder.append(value); + } else { + placeHolder.append("?"); + this.paramValues.add(value); + } + } + } + sql.append("INSERT INTO ")// + .append(entity.getTableName()).append(" (").append(fieldsPart).append(") VALUES (")// + .append(placeHolder.toString()).append(")"); + + return this; + } + + /** + * 删除 + * + * @param tableName 表名 + * @return 自己 + */ + public SqlBuilder delete(String tableName) { + if (StrUtil.isBlank(tableName)) { + throw new DbRuntimeException("Table name is blank !"); + } + + if (null != wrapper) { + // 包装表名 + tableName = wrapper.wrap(tableName); + } + + sql.append("DELETE FROM ").append(tableName); + + return this; + } + + /** + * 更新 + * + * @param entity 要更新的实体 + * @return 自己 + */ + public SqlBuilder update(Entity entity) { + // 验证 + validateEntity(entity); + + if (null != wrapper) { + // 包装表名 + // entity = wrapper.wrap(entity); + entity.setTableName(wrapper.wrap(entity.getTableName())); + } + + sql.append("UPDATE ").append(entity.getTableName()).append(" SET "); + String field; + for (Entry entry : entity.entrySet()) { + field = entry.getKey(); + if (StrUtil.isNotBlank(field)) { + if (paramValues.size() > 0) { + sql.append(", "); + } + this.fields.add(field); + sql.append((null != wrapper) ? wrapper.wrap(field) : field).append(" = ? "); + this.paramValues.add(entry.getValue());// 更新不对空做处理,因为存在清空字段的情况 + } + } + + return this; + } + + /** + * 查询 + * + * @param isDistinct 是否添加DISTINCT关键字(查询唯一结果) + * @param fields 查询的字段 + * @return 自己 + */ + public SqlBuilder select(boolean isDistinct, String... fields) { + return select(isDistinct, Arrays.asList(fields)); + } + + /** + * 查询 + * + * @param isDistinct 是否添加DISTINCT关键字(查询唯一结果) + * @param fields 查询的字段 + * @return 自己 + */ + public SqlBuilder select(boolean isDistinct, Collection fields) { + sql.append("SELECT "); + if (isDistinct) { + sql.append("DISTINCT "); + } + + if (CollectionUtil.isEmpty(fields)) { + sql.append("*"); + } else { + if (null != wrapper) { + // 包装字段名 + fields = wrapper.wrap(fields); + } + sql.append(CollectionUtil.join(fields, StrUtil.COMMA)); + } + + return this; + } + + /** + * 查询(非Distinct) + * + * @param fields 查询的字段 + * @return 自己 + */ + public SqlBuilder select(String... fields) { + return select(false, fields); + } + + /** + * 查询(非Distinct) + * + * @param fields 查询的字段 + * @return 自己 + */ + public SqlBuilder select(Collection fields) { + return select(false, fields); + } + + /** + * 添加 from语句 + * + * @param tableNames 表名列表(多个表名用于多表查询) + * @return 自己 + */ + public SqlBuilder from(String... tableNames) { + if (ArrayUtil.isEmpty(tableNames) || StrUtil.hasBlank(tableNames)) { + throw new DbRuntimeException("Table name is blank in table names !"); + } + + if (null != wrapper) { + // 包装表名 + tableNames = wrapper.wrap(tableNames); + } + + sql.append(" FROM ").append(ArrayUtil.join(tableNames, StrUtil.COMMA)); + + return this; + } + + /** + * 添加Where语句,所有逻辑之间为AND的关系 + * + * @param conditions 条件,当条件为空时,只添加WHERE关键字 + * @return 自己 + * @since 4.4.4 + */ + public SqlBuilder where(Condition... conditions) { + return where(LogicalOperator.AND, conditions); + } + + /** + * 添加Where语句
+ * 只支持单一的逻辑运算符(例如多个条件之间) + * + * @param logicalOperator 逻辑运算符 + * @param conditions 条件,当条件为空时,只添加WHERE关键字 + * @return 自己 + */ + public SqlBuilder where(LogicalOperator logicalOperator, Condition... conditions) { + if (ArrayUtil.isNotEmpty(conditions)) { + if (null != wrapper) { + // 包装字段名 + conditions = wrapper.wrap(conditions); + } + where(buildCondition(logicalOperator, conditions)); + } + + return this; + } + + /** + * 添加Where语句
+ * + * @param where WHERE语句之后跟的条件语句字符串 + * @return 自己 + */ + public SqlBuilder where(String where) { + if (StrUtil.isNotBlank(where)) { + sql.append(" WHERE ").append(where); + } + return this; + } + + /** + * 多值选择 + * + * @param 值类型 + * @param field 字段名 + * @param values 值列表 + * @return 自身 + */ + @SuppressWarnings("unchecked") + public SqlBuilder in(String field, T... values) { + sql.append(wrapper.wrap(field)).append(" IN ").append("(").append(ArrayUtil.join(values, StrUtil.COMMA)).append(")"); + return this; + } + + /** + * 分组 + * + * @param fields 字段 + * @return 自己 + */ + public SqlBuilder groupBy(String... fields) { + if (ArrayUtil.isNotEmpty(fields)) { + if (null != wrapper) { + // 包装字段名 + fields = wrapper.wrap(fields); + } + + sql.append(" GROUP BY ").append(ArrayUtil.join(fields, StrUtil.COMMA)); + } + + return this; + } + + /** + * 添加Having语句 + * + * @param logicalOperator 逻辑运算符 + * @param conditions 条件 + * @return 自己 + */ + public SqlBuilder having(LogicalOperator logicalOperator, Condition... conditions) { + if (ArrayUtil.isNotEmpty(conditions)) { + if (null != wrapper) { + // 包装字段名 + conditions = wrapper.wrap(conditions); + } + having(buildCondition(logicalOperator, conditions)); + } + + return this; + } + + /** + * 添加Having语句 + * + * @param having 条件语句 + * @return 自己 + */ + public SqlBuilder having(String having) { + if (StrUtil.isNotBlank(having)) { + sql.append(" HAVING ").append(having); + } + return this; + } + + /** + * 排序 + * + * @param orders 排序对象 + * @return 自己 + */ + public SqlBuilder orderBy(Order... orders) { + if (ArrayUtil.isEmpty(orders)) { + return this; + } + + sql.append(" ORDER BY "); + String field = null; + boolean isFirst = true; + for (Order order : orders) { + if (null != wrapper) { + // 包装字段名 + field = wrapper.wrap(order.getField()); + } + if (StrUtil.isBlank(field)) { + continue; + } + + // 只有在非第一项前添加逗号 + if (isFirst) { + isFirst = false; + } else { + sql.append(StrUtil.COMMA); + } + sql.append(field); + final Direction direction = order.getDirection(); + if (null != direction) { + sql.append(StrUtil.SPACE).append(direction); + } + } + return this; + } + + /** + * 多表关联 + * + * @param tableName 被关联的表名 + * @param join 内联方式 + * @return 自己 + */ + public SqlBuilder join(String tableName, Join join) { + if (StrUtil.isBlank(tableName)) { + throw new DbRuntimeException("Table name is blank !"); + } + + if (null != join) { + sql.append(StrUtil.SPACE).append(join).append(" JOIN "); + if (null != wrapper) { + // 包装表名 + tableName = wrapper.wrap(tableName); + } + sql.append(tableName); + } + return this; + } + + /** + * 配合JOIN的 ON语句,多表关联的条件语句
+ * 只支持单一的逻辑运算符(例如多个条件之间) + * + * @param logicalOperator 逻辑运算符 + * @param conditions 条件 + * @return 自己 + */ + public SqlBuilder on(LogicalOperator logicalOperator, Condition... conditions) { + if (ArrayUtil.isNotEmpty(conditions)) { + if (null != wrapper) { + // 包装字段名 + conditions = wrapper.wrap(conditions); + } + on(buildCondition(logicalOperator, conditions)); + } + + return this; + } + + /** + * 配合JOIN的 ON语句,多表关联的条件语句
+ * 只支持单一的逻辑运算符(例如多个条件之间) + * + * @param on 条件 + * @return 自己 + */ + public SqlBuilder on(String on) { + if (StrUtil.isNotBlank(on)) { + this.sql.append(" ON ").append(on); + } + return this; + } + + /** + * 在SQL的开头补充SQL片段 + * + * @param sqlFragment SQL片段 + * @return this + * @since 4.1.3 + */ + public SqlBuilder insertPreFragment(Object sqlFragment) { + if (null != sqlFragment) { + this.sql.insert(0, sqlFragment); + } + return this; + } + + /** + * 追加SQL其它部分片段 + * + * @param sqlFragment SQL其它部分片段 + * @return this + */ + public SqlBuilder append(Object sqlFragment) { + if (null != sqlFragment) { + this.sql.append(sqlFragment); + } + return this; + } + + /** + * 构建查询SQL + * + * @param query {@link Query} + * @return this + */ + public SqlBuilder query(Query query) { + return this.select(query.getFields()).from(query.getTableNames()).where(LogicalOperator.AND, query.getWhere()); + } + // --------------------------------------------------------------- Builder end + + /** + * 获得插入或更新的数据库字段列表 + * + * @return 插入或更新的数据库字段列表 + */ + public List getFields() { + return this.fields; + } + + /** + * 获得插入或更新的数据库字段列表 + * + * @return 插入或更新的数据库字段列表 + */ + public String[] getFieldArray() { + return this.fields.toArray(new String[this.fields.size()]); + } + + /** + * 获得占位符对应的值列表
+ * + * @return 占位符对应的值列表 + */ + public List getParamValues() { + return this.paramValues; + } + + /** + * 获得占位符对应的值列表
+ * + * @return 占位符对应的值列表 + */ + public Object[] getParamValueArray() { + return this.paramValues.toArray(new Object[this.paramValues.size()]); + } + + /** + * 构建,默认打印SQL日志 + * + * @return 构建好的SQL语句 + */ + @Override + public String build() { + return this.sql.toString(); + } + + @Override + public String toString() { + return this.build(); + } + + // --------------------------------------------------------------- private method start + /** + * 构建组合条件
+ * 例如:name = ? AND type IN (?, ?) AND other LIKE ? + * + * @param logicalOperator 逻辑运算符 + * @param conditions 条件对象 + * @return 构建后的SQL语句条件部分 + */ + private String buildCondition(LogicalOperator logicalOperator, Condition... conditions) { + if (ArrayUtil.isEmpty(conditions)) { + return StrUtil.EMPTY; + } + if (null == logicalOperator) { + logicalOperator = LogicalOperator.AND; + } + + final StringBuilder conditionStrBuilder = new StringBuilder(); + boolean isFirst = true; + for (Condition condition : conditions) { + // 添加逻辑运算符 + if (isFirst) { + isFirst = false; + } else { + // " AND " 或者 " OR " + conditionStrBuilder.append(StrUtil.SPACE).append(logicalOperator).append(StrUtil.SPACE); + } + + // 构建条件部分:"name = ?"、"name IN (?,?,?)"、"name BETWEEN ?AND ?"、"name LIKE ?" + conditionStrBuilder.append(condition.toString(this.paramValues)); + } + + return conditionStrBuilder.toString(); + } + + /** + * 验证实体类对象的有效性 + * + * @param entity 实体类对象 + * @throws DbRuntimeException SQL异常包装,获取元数据信息失败 + */ + private static void validateEntity(Entity entity) throws DbRuntimeException { + if (null == entity) { + throw new DbRuntimeException("Entity is null !"); + } + if (StrUtil.isBlank(entity.getTableName())) { + throw new DbRuntimeException("Entity`s table name is null !"); + } + if (entity.isEmpty()) { + throw new DbRuntimeException("No filed and value in this entity !"); + } + } + // --------------------------------------------------------------- private method end +} diff --git a/hutool-db/src/main/java/cn/hutool/db/sql/SqlExecutor.java b/hutool-db/src/main/java/cn/hutool/db/sql/SqlExecutor.java new file mode 100644 index 000000000..831f3bc2b --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/sql/SqlExecutor.java @@ -0,0 +1,355 @@ +package cn.hutool.db.sql; + +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Map; + +import cn.hutool.core.collection.ArrayIter; +import cn.hutool.db.DbUtil; +import cn.hutool.db.StatementUtil; +import cn.hutool.db.handler.RsHandler; + +/** + * SQL执行器,全部为静态方法,执行查询或非查询的SQL语句
+ * 此方法为JDBC的简单封装,与数据库类型无关 + * + * @author loolly + * + */ +public class SqlExecutor { + + /** + * 执行非查询语句
+ * 语句包括 插入、更新、删除
+ * 此方法不会关闭Connection + * + * @param conn 数据库连接对象 + * @param sql SQL,使用name做为占位符,例如:name + * @param paramMap 参数Map + * @return 影响的行数 + * @throws SQLException SQL执行异常 + * @since 4.0.10 + */ + public static int execute(Connection conn, String sql, Map paramMap) throws SQLException { + final NamedSql namedSql = new NamedSql(sql, paramMap); + return execute(conn, namedSql.getSql(), namedSql.getParams()); + } + + /** + * 执行非查询语句
+ * 语句包括 插入、更新、删除
+ * 此方法不会关闭Connection + * + * @param conn 数据库连接对象 + * @param sql SQL + * @param params 参数 + * @return 影响的行数 + * @throws SQLException SQL执行异常 + */ + public static int execute(Connection conn, String sql, Object... params) throws SQLException { + PreparedStatement ps = null; + try { + ps = StatementUtil.prepareStatement(conn, sql, params); + return ps.executeUpdate(); + } finally { + DbUtil.close(ps); + } + } + + /** + * 执行调用存储过程
+ * 此方法不会关闭Connection + * + * @param conn 数据库连接对象 + * @param sql SQL + * @param params 参数 + * @return 如果执行后第一个结果是ResultSet,则返回true,否则返回false。 + * @throws SQLException SQL执行异常 + */ + public static boolean call(Connection conn, String sql, Object... params) throws SQLException { + CallableStatement call = null; + try { + call = StatementUtil.prepareCall(conn, sql, params); + return call.execute(); + } finally { + DbUtil.close(call); + } + } + + /** + * 执行调用存储过程
+ * 此方法不会关闭Connection + * + * @param conn 数据库连接对象 + * @param sql SQL + * @param params 参数 + * @return ResultSet + * @throws SQLException SQL执行异常 + * @since 4.1.4 + */ + public static ResultSet callQuery(Connection conn, String sql, Object... params) throws SQLException { + CallableStatement proc = null; + try { + proc = StatementUtil.prepareCall(conn, sql, params); + return proc.executeQuery(); + } finally { + DbUtil.close(proc); + } + } + + /** + * 执行非查询语句,返回主键
+ * 发查询语句包括 插入、更新、删除
+ * 此方法不会关闭Connection + * + * @param conn 数据库连接对象 + * @param sql SQL + * @param paramMap 参数Map + * @return 主键 + * @throws SQLException SQL执行异常 + * @since 4.0.10 + */ + public static Long executeForGeneratedKey(Connection conn, String sql, Map paramMap) throws SQLException { + final NamedSql namedSql = new NamedSql(sql, paramMap); + return executeForGeneratedKey(conn, namedSql.getSql(), namedSql.getParams()); + } + + /** + * 执行非查询语句,返回主键
+ * 发查询语句包括 插入、更新、删除
+ * 此方法不会关闭Connection + * + * @param conn 数据库连接对象 + * @param sql SQL + * @param params 参数 + * @return 主键 + * @throws SQLException SQL执行异常 + */ + public static Long executeForGeneratedKey(Connection conn, String sql, Object... params) throws SQLException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + ps = StatementUtil.prepareStatement(conn, sql, params); + ps.executeUpdate(); + rs = ps.getGeneratedKeys(); + if (rs != null && rs.next()) { + try { + return rs.getLong(1); + } catch (SQLException e) { + // 可能会出现没有主键返回的情况 + } + } + return null; + } finally { + DbUtil.close(ps); + DbUtil.close(rs); + } + } + + /** + * 批量执行非查询语句
+ * 语句包括 插入、更新、删除
+ * 此方法不会关闭Connection + * + * @param conn 数据库连接对象 + * @param sql SQL + * @param paramsBatch 批量的参数 + * @return 每个SQL执行影响的行数 + * @throws SQLException SQL执行异常 + */ + public static int[] executeBatch(Connection conn, String sql, Object[]... paramsBatch) throws SQLException { + return executeBatch(conn, sql, new ArrayIter(paramsBatch)); + } + + /** + * 批量执行非查询语句
+ * 语句包括 插入、更新、删除
+ * 此方法不会关闭Connection + * + * @param conn 数据库连接对象 + * @param sql SQL + * @param paramsBatch 批量的参数 + * @return 每个SQL执行影响的行数 + * @throws SQLException SQL执行异常 + */ + public static int[] executeBatch(Connection conn, String sql, Iterable paramsBatch) throws SQLException { + PreparedStatement ps = null; + try { + ps = StatementUtil.prepareStatementForBatch(conn, sql, paramsBatch); + return ps.executeBatch(); + } finally { + DbUtil.close(ps); + } + } + + /** + * 批量执行非查询语句
+ * 语句包括 插入、更新、删除
+ * 此方法不会关闭Connection + * + * @param conn 数据库连接对象 + * @param sqls SQL列表 + * @return 每个SQL执行影响的行数 + * @throws SQLException SQL执行异常 + * @since 4.5.6 + */ + public static int[] executeBatch(Connection conn, String... sqls) throws SQLException { + return executeBatch(conn, new ArrayIter(sqls)); + } + + /** + * 批量执行非查询语句
+ * 语句包括 插入、更新、删除
+ * 此方法不会关闭Connection + * + * @param conn 数据库连接对象 + * @param sqls SQL列表 + * @return 每个SQL执行影响的行数 + * @throws SQLException SQL执行异常 + * @since 4.5.6 + */ + public static int[] executeBatch(Connection conn, Iterable sqls) throws SQLException { + Statement statement = null; + try { + statement = conn.createStatement(); + for (String sql : sqls) { + statement.addBatch(sql); + } + return statement.executeBatch(); + } finally { + DbUtil.close(statement); + } + } + + /** + * 执行查询语句
+ * 此方法不会关闭Connection + * + * @param 处理结果类型 + * @param conn 数据库连接对象 + * @param sql 查询语句,使用参数名占位符,例如:name + * @param rsh 结果集处理对象 + * @param paramMap 参数对 + * @return 结果对象 + * @throws SQLException SQL执行异常 + * @since 4.0.10 + */ + public static T query(Connection conn, String sql, RsHandler rsh, Map paramMap) throws SQLException { + final NamedSql namedSql = new NamedSql(sql, paramMap); + return query(conn, namedSql.getSql(), rsh, namedSql.getParams()); + } + + /** + * 执行查询语句
+ * 此方法不会关闭Connection + * + * @param 处理结果类型 + * @param conn 数据库连接对象 + * @param sql 查询语句 + * @param rsh 结果集处理对象 + * @param params 参数 + * @return 结果对象 + * @throws SQLException SQL执行异常 + */ + public static T query(Connection conn, String sql, RsHandler rsh, Object... params) throws SQLException { + PreparedStatement ps = null; + try { + ps = StatementUtil.prepareStatement(conn, sql, params); + return executeQuery(ps, rsh); + } finally { + DbUtil.close(ps); + } + } + + // -------------------------------------------------------------------------------------- Execute With PreparedStatement + /** + * 用于执行 INSERT、UPDATE 或 DELETE 语句以及 SQL DDL(数据定义语言)语句,例如 CREATE TABLE 和 DROP TABLE。
+ * INSERT、UPDATE 或 DELETE 语句的效果是修改表中零行或多行中的一列或多列。
+ * executeUpdate 的返回值是一个整数(int),指示受影响的行数(即更新计数)。
+ * 对于 CREATE TABLE 或 DROP TABLE 等不操作行的语句,executeUpdate 的返回值总为零。
+ * 此方法不会关闭PreparedStatement + * + * @param ps PreparedStatement对象 + * @param params 参数 + * @return 影响的行数 + * @throws SQLException SQL执行异常 + */ + public static int executeUpdate(PreparedStatement ps, Object... params) throws SQLException { + StatementUtil.fillParams(ps, params); + return ps.executeUpdate(); + } + + /** + * 可用于执行任何SQL语句,返回一个boolean值,表明执行该SQL语句是否返回了ResultSet。
+ * 如果执行后第一个结果是ResultSet,则返回true,否则返回false。
+ * 此方法不会关闭PreparedStatement + * + * @param ps PreparedStatement对象 + * @param params 参数 + * @return 如果执行后第一个结果是ResultSet,则返回true,否则返回false。 + * @throws SQLException SQL执行异常 + */ + public static boolean execute(PreparedStatement ps, Object... params) throws SQLException { + StatementUtil.fillParams(ps, params); + return ps.execute(); + } + + /** + * 执行查询语句
+ * 此方法不会关闭PreparedStatement + * + * @param 处理结果类型 + * @param ps PreparedStatement + * @param rsh 结果集处理对象 + * @param params 参数 + * @return 结果对象 + * @throws SQLException SQL执行异常 + */ + public static T query(PreparedStatement ps, RsHandler rsh, Object... params) throws SQLException { + StatementUtil.fillParams(ps, params); + return executeQuery(ps, rsh); + } + + /** + * 执行查询语句并关闭PreparedStatement + * + * @param 处理结果类型 + * @param ps PreparedStatement + * @param rsh 结果集处理对象 + * @param params 参数 + * @return 结果对象 + * @throws SQLException SQL执行异常 + */ + public static T queryAndClosePs(PreparedStatement ps, RsHandler rsh, Object... params) throws SQLException { + try { + return query(ps, rsh, params); + } finally { + DbUtil.close(ps); + } + } + + // -------------------------------------------------------------------------------------------------------------------------------- Private method start + /** + * 执行查询 + * + * @param ps {@link PreparedStatement} + * @param rsh 结果集处理对象 + * @return 结果对象 + * @throws SQLException SQL执行异常 + * @since 4.1.13 + */ + private static T executeQuery(PreparedStatement ps, RsHandler rsh) throws SQLException { + ResultSet rs = null; + try { + rs = ps.executeQuery(); + return rsh.handle(rs); + } finally { + DbUtil.close(rs); + } + } + // -------------------------------------------------------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-db/src/main/java/cn/hutool/db/sql/SqlFormatter.java b/hutool-db/src/main/java/cn/hutool/db/sql/SqlFormatter.java new file mode 100644 index 000000000..3a77e1b66 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/sql/SqlFormatter.java @@ -0,0 +1,327 @@ +package cn.hutool.db.sql; + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Set; +import java.util.StringTokenizer; + +/** + * SQL格式化器 from Hibernate + * @author looly + */ +public class SqlFormatter { + private static final Set BEGIN_CLAUSES = new HashSet(); + private static final Set END_CLAUSES = new HashSet(); + private static final Set LOGICAL = new HashSet(); + private static final Set QUANTIFIERS = new HashSet(); + private static final Set DML = new HashSet(); + private static final Set MISC = new HashSet(); + + static { + BEGIN_CLAUSES.add("left"); + BEGIN_CLAUSES.add("right"); + BEGIN_CLAUSES.add("inner"); + BEGIN_CLAUSES.add("outer"); + BEGIN_CLAUSES.add("group"); + BEGIN_CLAUSES.add("order"); + + END_CLAUSES.add("where"); + END_CLAUSES.add("set"); + END_CLAUSES.add("having"); + END_CLAUSES.add("join"); + END_CLAUSES.add("from"); + END_CLAUSES.add("by"); + END_CLAUSES.add("join"); + END_CLAUSES.add("into"); + END_CLAUSES.add("union"); + + LOGICAL.add("and"); + LOGICAL.add("or"); + LOGICAL.add("when"); + LOGICAL.add("else"); + LOGICAL.add("end"); + + QUANTIFIERS.add("in"); + QUANTIFIERS.add("all"); + QUANTIFIERS.add("exists"); + QUANTIFIERS.add("some"); + QUANTIFIERS.add("any"); + + DML.add("insert"); + DML.add("update"); + DML.add("delete"); + + MISC.add("select"); + MISC.add("on"); + } + + private static String indentString = " "; + private static String initial = "\n "; + + public static String format(String source) { + return new FormatProcess(source).perform().trim(); + } + + //------------------------------------------------------------------------------------------------ + + private static class FormatProcess { + boolean beginLine = true; + boolean afterBeginBeforeEnd = false; + boolean afterByOrSetOrFromOrSelect = false; +// boolean afterValues = false; + boolean afterOn = false; + boolean afterBetween = false; + boolean afterInsert = false; + int inFunction = 0; + int parensSinceSelect = 0; + private LinkedList parenCounts = new LinkedList(); + private LinkedList afterByOrFromOrSelects = new LinkedList(); + + int indent = 1; + + StringBuffer result = new StringBuffer(); + StringTokenizer tokens; + String lastToken; + String token; + String lcToken; + + public FormatProcess(String sql) { + this.tokens = new StringTokenizer(sql, "()+*/-=<>'`\"[], \n\r\f\t", true); + } + + public String perform() { + this.result.append(initial); + + while (this.tokens.hasMoreTokens()) { + this.token = this.tokens.nextToken(); + this.lcToken = this.token.toLowerCase(); + + if ("'".equals(this.token)) { + String t; + do { + t = this.tokens.nextToken(); + this.token += t; + } while ((!"'".equals(t)) && (this.tokens.hasMoreTokens())); + } else if ("\"".equals(this.token)) { + String t; + do { + t = this.tokens.nextToken(); + this.token += t; + } while (!"\"".equals(t)); + } + + if ((this.afterByOrSetOrFromOrSelect) && (",".equals(this.token))) { + commaAfterByOrFromOrSelect(); + } else if ((this.afterOn) && (",".equals(this.token))) { + commaAfterOn(); + } else if ("(".equals(this.token)) { + openParen(); + } else if (")".equals(this.token)) { + closeParen(); + } else if (BEGIN_CLAUSES.contains(this.lcToken)) { + beginNewClause(); + } else if (END_CLAUSES.contains(this.lcToken)) { + endNewClause(); + } else if ("select".equals(this.lcToken)) { + select(); + } else if (DML.contains(this.lcToken)) { + updateOrInsertOrDelete(); + } else if ("values".equals(this.lcToken)) { + values(); + } else if ("on".equals(this.lcToken)) { + on(); + } else if ((this.afterBetween) && (this.lcToken.equals("and"))) { + misc(); + this.afterBetween = false; + } else if (LOGICAL.contains(this.lcToken)) { + logical(); + } else if (isWhitespace(this.token)) { + white(); + } else { + misc(); + } + + if (!isWhitespace(this.token)) { + this.lastToken = this.lcToken; + } + } + + return this.result.toString(); + } + + private void commaAfterOn() { + out(); + this.indent -= 1; + newline(); + this.afterOn = false; + this.afterByOrSetOrFromOrSelect = true; + } + + private void commaAfterByOrFromOrSelect() { + out(); + newline(); + } + + private void logical() { + if ("end".equals(this.lcToken)) { + this.indent -= 1; + } + newline(); + out(); + this.beginLine = false; + } + + private void on() { + this.indent += 1; + this.afterOn = true; + newline(); + out(); + this.beginLine = false; + } + + private void misc() { + out(); + if ("between".equals(this.lcToken)) { + this.afterBetween = true; + } + if (this.afterInsert) { + newline(); + this.afterInsert = false; + } else { + this.beginLine = false; + if ("case".equals(this.lcToken)) { + this.indent += 1; + } + } + } + + private void white() { + if (!this.beginLine) { + this.result.append(" "); + } + } + + private void updateOrInsertOrDelete() { + out(); + this.indent += 1; + this.beginLine = false; + if ("update".equals(this.lcToken)) { + newline(); + } + if ("insert".equals(this.lcToken)) { + this.afterInsert = true; + } + } + + private void select() { + out(); + this.indent += 1; + newline(); + this.parenCounts.addLast(new Integer(this.parensSinceSelect)); + this.afterByOrFromOrSelects.addLast(Boolean.valueOf(this.afterByOrSetOrFromOrSelect)); + this.parensSinceSelect = 0; + this.afterByOrSetOrFromOrSelect = true; + } + + private void out() { + this.result.append(this.token); + } + + private void endNewClause() { + if (!this.afterBeginBeforeEnd) { + this.indent -= 1; + if (this.afterOn) { + this.indent -= 1; + this.afterOn = false; + } + newline(); + } + out(); + if (!"union".equals(this.lcToken)) { + this.indent += 1; + } + newline(); + this.afterBeginBeforeEnd = false; + this.afterByOrSetOrFromOrSelect = (("by".equals(this.lcToken)) || ("set".equals(this.lcToken)) || ("from".equals(this.lcToken))); + } + + private void beginNewClause() { + if (!this.afterBeginBeforeEnd) { + if (this.afterOn) { + this.indent -= 1; + this.afterOn = false; + } + this.indent -= 1; + newline(); + } + out(); + this.beginLine = false; + this.afterBeginBeforeEnd = true; + } + + private void values() { + this.indent -= 1; + newline(); + out(); + this.indent += 1; + newline(); +// this.afterValues = true; + } + + private void closeParen() { + this.parensSinceSelect -= 1; + if (this.parensSinceSelect < 0) { + this.indent -= 1; + this.parensSinceSelect = ((Integer) this.parenCounts.removeLast()).intValue(); + this.afterByOrSetOrFromOrSelect = ((Boolean) this.afterByOrFromOrSelects.removeLast()).booleanValue(); + } + if (this.inFunction > 0) { + this.inFunction -= 1; + out(); + } else { + if (!this.afterByOrSetOrFromOrSelect) { + this.indent -= 1; + newline(); + } + out(); + } + this.beginLine = false; + } + + private void openParen() { + if ((isFunctionName(this.lastToken)) || (this.inFunction > 0)) { + this.inFunction += 1; + } + this.beginLine = false; + if (this.inFunction > 0) { + out(); + } else { + out(); + if (!this.afterByOrSetOrFromOrSelect) { + this.indent += 1; + newline(); + this.beginLine = true; + } + } + this.parensSinceSelect += 1; + } + + private static boolean isFunctionName(String tok) { + char begin = tok.charAt(0); + boolean isIdentifier = (Character.isJavaIdentifierStart(begin)) || ('"' == begin); + return (isIdentifier) && (!LOGICAL.contains(tok)) && (!END_CLAUSES.contains(tok)) && (!QUANTIFIERS.contains(tok)) && (!DML.contains(tok)) && (!MISC.contains(tok)); + } + + private static boolean isWhitespace(String token) { + return " \n\r\f\t".indexOf(token) >= 0; + } + + private void newline() { + this.result.append("\n"); + for (int i = 0; i < this.indent; i++) { + this.result.append(indentString); + } + this.beginLine = true; + } + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/sql/SqlLog.java b/hutool-db/src/main/java/cn/hutool/db/sql/SqlLog.java new file mode 100644 index 000000000..3159d0853 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/sql/SqlLog.java @@ -0,0 +1,56 @@ +package cn.hutool.db.sql; + +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import cn.hutool.log.level.Level; + +/** + * SQL在日志中打印配置 + * + * @author looly + * @since 4.1.0 + */ +public enum SqlLog { + INSTASNCE; + + private final static Log log = LogFactory.get(); + + /** 是否debugSQL */ + private boolean showSql; + /** 是否格式化SQL */ + private boolean formatSql; + /** 是否显示参数 */ + private boolean showParams; + /** 默认日志级别 */ + private Level level = Level.DEBUG; + + /** + * 设置全局配置:是否通过debug日志显示SQL + * + * @param isShowSql 是否显示SQL + * @param isFormatSql 是否格式化显示的SQL + * @param isShowParams 是否打印参数 + */ + public void init(boolean isShowSql, boolean isFormatSql, boolean isShowParams, Level level) { + this.showSql = isShowSql; + this.formatSql = isFormatSql; + this.showParams = isShowParams; + this.level = level; + } + + /** + * 打印SQL日志 + * + * @param sql SQL语句 + * @param paramValues 参数,可为null + */ + public void log(String sql, Object paramValues) { + if (this.showSql) { + if (this.showParams) { + log.log(this.level, "\nSQL -> {}\nParams -> {}", this.formatSql ? SqlFormatter.format(sql) : sql, paramValues); + } else { + log.log(this.level, "\nSQL -> {}", this.formatSql ? SqlFormatter.format(sql) : sql); + } + } + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/sql/SqlUtil.java b/hutool-db/src/main/java/cn/hutool/db/sql/SqlUtil.java new file mode 100644 index 000000000..13557d07e --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/sql/SqlUtil.java @@ -0,0 +1,254 @@ +package cn.hutool.db.sql; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.nio.charset.Charset; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.RowId; +import java.sql.SQLException; +import java.util.List; +import java.util.Map.Entry; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.db.DbRuntimeException; +import cn.hutool.db.Entity; +import cn.hutool.db.sql.Condition.LikeType; + +/** + * SQL相关工具类,包括相关SQL语句拼接等 + * + * @author looly + * @since 4.0.10 + */ +public class SqlUtil { + + /** + * 构件相等条件的where语句
+ * 如果没有条件语句,泽返回空串,表示没有条件 + * + * @param entity 条件实体 + * @param paramValues 条件值得存放List + * @return 带where关键字的SQL部分 + */ + public static String buildEqualsWhere(Entity entity, List paramValues) { + if (null == entity || entity.isEmpty()) { + return StrUtil.EMPTY; + } + + final StringBuilder sb = new StringBuilder(" WHERE "); + boolean isNotFirst = false; + for (Entry entry : entity.entrySet()) { + if (isNotFirst) { + sb.append(" and "); + } else { + isNotFirst = true; + } + sb.append("`").append(entry.getKey()).append("`").append(" = ?"); + paramValues.add(entry.getValue()); + } + + return sb.toString(); + } + + /** + * 通过实体对象构建条件对象 + * + * @param entity 实体对象 + * @return 条件对象 + */ + public static Condition[] buildConditions(Entity entity) { + if (null == entity || entity.isEmpty()) { + return null; + } + + final Condition[] conditions = new Condition[entity.size()]; + int i = 0; + Object value; + for (Entry entry : entity.entrySet()) { + value = entry.getValue(); + if (value instanceof Condition) { + conditions[i++] = (Condition) value; + } else { + conditions[i++] = new Condition(entry.getKey(), value); + } + } + + return conditions; + } + + /** + * 创建LIKE语句中的值,创建的结果为: + * + *
+	 * 1、LikeType.StartWith: %value
+	 * 2、LikeType.EndWith: value%
+	 * 3、LikeType.Contains: %value%
+	 * 
+ * + * 如果withLikeKeyword为true,则结果为: + * + *
+	 * 1、LikeType.StartWith: LIKE %value
+	 * 2、LikeType.EndWith: LIKE value%
+	 * 3、LikeType.Contains: LIKE %value%
+	 * 
+ * + * @param value 被查找值 + * @param likeType LIKE值类型 {@link LikeType} + * @return 拼接后的like值 + */ + public static String buildLikeValue(String value, LikeType likeType, boolean withLikeKeyword) { + if (null == value) { + return value; + } + + StringBuilder likeValue = StrUtil.builder(withLikeKeyword ? "LIKE " : ""); + switch (likeType) { + case StartWith: + likeValue.append('%').append(value); + break; + case EndWith: + likeValue.append(value).append('%'); + break; + case Contains: + likeValue.append('%').append(value).append('%'); + break; + + default: + break; + } + return likeValue.toString(); + } + + /** + * 格式化SQL + * + * @param sql SQL + * @return 格式化后的SQL + */ + public static String formatSql(String sql) { + return SqlFormatter.format(sql); + } + + /** + * 将RowId转为字符串 + * + * @param rowId RowId + * @return RowId字符串 + */ + public static String rowIdToString(RowId rowId) { + return StrUtil.str(rowId.getBytes(), CharsetUtil.CHARSET_ISO_8859_1); + } + + /** + * Clob字段值转字符串 + * + * @param clob {@link Clob} + * @return 字符串 + * @since 3.0.6 + */ + public static String clobToStr(Clob clob) { + Reader reader = null; + try { + reader = clob.getCharacterStream(); + return IoUtil.read(reader); + } catch (SQLException e) { + throw new DbRuntimeException(e); + } finally { + IoUtil.close(reader); + } + } + + /** + * Blob字段值转字符串 + * + * @param blob {@link Blob} + * @param charset 编码 + * @return 字符串 + * @since 3.0.6 + */ + public static String blobToStr(Blob blob, Charset charset) { + InputStream in = null; + try { + in = blob.getBinaryStream(); + return IoUtil.read(in, charset); + } catch (SQLException e) { + throw new DbRuntimeException(e); + } finally { + IoUtil.close(in); + } + } + + /** + * 创建Blob对象 + * + * @param conn {@link Connection} + * @param dataStream 数据流,使用完毕后关闭 + * @param closeAfterUse 使用完毕是否关闭流 + * @return {@link Blob} + * @since 4.5.13 + */ + public static Blob createBlob(Connection conn, InputStream dataStream, boolean closeAfterUse) { + Blob blob; + OutputStream out = null; + try { + blob = conn.createBlob(); + out = blob.setBinaryStream(1); + IoUtil.copy(dataStream, out); + } catch (SQLException e) { + throw new DbRuntimeException(e); + } finally { + IoUtil.close(out); + if (closeAfterUse) { + IoUtil.close(dataStream); + } + } + return blob; + } + + /** + * 创建Blob对象 + * + * @param conn {@link Connection} + * @param data 数据 + * @return {@link Blob} + * @since 4.5.13 + */ + public static Blob createBlob(Connection conn, byte[] data) { + Blob blob; + try { + blob = conn.createBlob(); + blob.setBytes(0, data); + } catch (SQLException e) { + throw new DbRuntimeException(e); + } + return blob; + } + + /** + * 转换为{@link java.sql.Date} + * + * @param date {@link java.util.Date} + * @return {@link java.sql.Date} + * @since 3.1.2 + */ + public static java.sql.Date toSqlDate(java.util.Date date) { + return new java.sql.Date(date.getTime()); + } + + /** + * 转换为{@link java.sql.Timestamp} + * + * @param date {@link java.util.Date} + * @return {@link java.sql.Timestamp} + * @since 3.1.2 + */ + public static java.sql.Timestamp toSqlTimestamp(java.util.Date date) { + return new java.sql.Timestamp(date.getTime()); + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/sql/StatementWrapper.java b/hutool-db/src/main/java/cn/hutool/db/sql/StatementWrapper.java new file mode 100644 index 000000000..f04d07e5e --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/sql/StatementWrapper.java @@ -0,0 +1,539 @@ +package cn.hutool.db.sql; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.net.URL; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.Date; +import java.sql.NClob; +import java.sql.ParameterMetaData; +import java.sql.PreparedStatement; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; + +/** + * {@link PreparedStatement} 包装类,用于添加拦截方法功能
+ * 拦截方法包括: + * + *
+ * 1. 提供参数注入
+ * 2. 提供SQL打印日志拦截
+ * 
+ * + * @author looly + * @since 4.1.0 + */ +public class StatementWrapper implements PreparedStatement { + private PreparedStatement rawStatement; + + + + @Override + public ResultSet executeQuery(String sql) throws SQLException { + return rawStatement.executeQuery(sql); + } + + @Override + public int executeUpdate(String sql) throws SQLException { + return rawStatement.executeUpdate(sql); + } + + @Override + public void close() throws SQLException { + rawStatement.close(); + } + + @Override + public int getMaxFieldSize() throws SQLException { + return rawStatement.getMaxFieldSize(); + } + + @Override + public void setMaxFieldSize(int max) throws SQLException { + rawStatement.setMaxFieldSize(max); + } + + @Override + public int getMaxRows() throws SQLException { + return rawStatement.getMaxRows(); + } + + @Override + public void setMaxRows(int max) throws SQLException { + rawStatement.setMaxRows(max); + } + + @Override + public void setEscapeProcessing(boolean enable) throws SQLException { + rawStatement.setEscapeProcessing(enable); + } + + @Override + public int getQueryTimeout() throws SQLException { + return rawStatement.getQueryTimeout(); + } + + @Override + public void setQueryTimeout(int seconds) throws SQLException { + rawStatement.setQueryTimeout(seconds); + } + + @Override + public void cancel() throws SQLException { + rawStatement.cancel(); + } + + @Override + public SQLWarning getWarnings() throws SQLException { + return rawStatement.getWarnings(); + } + + @Override + public void clearWarnings() throws SQLException { + rawStatement.clearWarnings(); + } + + @Override + public void setCursorName(String name) throws SQLException { + rawStatement.setCursorName(name); + } + + @Override + public boolean execute(String sql) throws SQLException { + return rawStatement.execute(sql); + } + + @Override + public ResultSet getResultSet() throws SQLException { + return rawStatement.getResultSet(); + } + + @Override + public int getUpdateCount() throws SQLException { + return rawStatement.getUpdateCount(); + } + + @Override + public boolean getMoreResults() throws SQLException { + return rawStatement.getMoreResults(); + } + + @Override + public void setFetchDirection(int direction) throws SQLException { + rawStatement.setFetchDirection(direction); + } + + @Override + public int getFetchDirection() throws SQLException { + return rawStatement.getFetchDirection(); + } + + @Override + public void setFetchSize(int rows) throws SQLException { + rawStatement.setFetchSize(rows); + } + + @Override + public int getFetchSize() throws SQLException { + return rawStatement.getFetchSize(); + } + + @Override + public int getResultSetConcurrency() throws SQLException { + return rawStatement.getResultSetConcurrency(); + } + + @Override + public int getResultSetType() throws SQLException { + return rawStatement.getResultSetType(); + } + + @Override + public void addBatch(String sql) throws SQLException { + rawStatement.addBatch(sql); + } + + @Override + public void clearBatch() throws SQLException { + rawStatement.clearBatch(); + } + + @Override + public int[] executeBatch() throws SQLException { + return rawStatement.executeBatch(); + } + + @Override + public Connection getConnection() throws SQLException { + return rawStatement.getConnection(); + } + + @Override + public boolean getMoreResults(int current) throws SQLException { + return rawStatement.getMoreResults(current); + } + + @Override + public ResultSet getGeneratedKeys() throws SQLException { + return rawStatement.getGeneratedKeys(); + } + + @Override + public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { + return rawStatement.executeUpdate(sql, autoGeneratedKeys); + } + + @Override + public int executeUpdate(String sql, int[] columnIndexes) throws SQLException { + return rawStatement.executeUpdate(sql, columnIndexes); + } + + @Override + public int executeUpdate(String sql, String[] columnNames) throws SQLException { + return rawStatement.executeUpdate(sql, columnNames); + } + + @Override + public boolean execute(String sql, int autoGeneratedKeys) throws SQLException { + return rawStatement.execute(sql, autoGeneratedKeys); + } + + @Override + public boolean execute(String sql, int[] columnIndexes) throws SQLException { + return rawStatement.execute(sql, columnIndexes); + } + + @Override + public boolean execute(String sql, String[] columnNames) throws SQLException { + return rawStatement.execute(sql, columnNames); + } + + @Override + public int getResultSetHoldability() throws SQLException { + return rawStatement.getResultSetHoldability(); + } + + @Override + public boolean isClosed() throws SQLException { + return rawStatement.isClosed(); + } + + @Override + public void setPoolable(boolean poolable) throws SQLException { + rawStatement.setPoolable(poolable); + } + + @Override + public boolean isPoolable() throws SQLException { + return rawStatement.isPoolable(); + } + + @Override + public void closeOnCompletion() throws SQLException { + rawStatement.closeOnCompletion(); + } + + @Override + public boolean isCloseOnCompletion() throws SQLException { + return rawStatement.isCloseOnCompletion(); + } + + @Override + public T unwrap(Class iface) throws SQLException { + return rawStatement.unwrap(iface); + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return rawStatement.isWrapperFor(iface); + } + + @Override + public ResultSet executeQuery() throws SQLException { + return rawStatement.executeQuery(); + } + + @Override + public int executeUpdate() throws SQLException { + return rawStatement.executeUpdate(); + } + + @Override + public void setNull(int parameterIndex, int sqlType) throws SQLException { + rawStatement.setNull(parameterIndex, sqlType); + } + + @Override + public void setBoolean(int parameterIndex, boolean x) throws SQLException { + rawStatement.setBoolean(parameterIndex, x); + } + + @Override + public void setByte(int parameterIndex, byte x) throws SQLException { + rawStatement.setByte(parameterIndex, x); + } + + @Override + public void setShort(int parameterIndex, short x) throws SQLException { + rawStatement.setShort(parameterIndex, x); + } + + @Override + public void setInt(int parameterIndex, int x) throws SQLException { + rawStatement.setInt(parameterIndex, x); + } + + @Override + public void setLong(int parameterIndex, long x) throws SQLException { + rawStatement.setLong(parameterIndex, x); + } + + @Override + public void setFloat(int parameterIndex, float x) throws SQLException { + rawStatement.setFloat(parameterIndex, x); + } + + @Override + public void setDouble(int parameterIndex, double x) throws SQLException { + rawStatement.setDouble(parameterIndex, x); + } + + @Override + public void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException { + rawStatement.setBigDecimal(parameterIndex, x); + } + + @Override + public void setString(int parameterIndex, String x) throws SQLException { + rawStatement.setString(parameterIndex, x); + } + + @Override + public void setBytes(int parameterIndex, byte[] x) throws SQLException { + rawStatement.setBytes(parameterIndex, x); + } + + @Override + public void setDate(int parameterIndex, Date x) throws SQLException { + rawStatement.setDate(parameterIndex, x); + } + + @Override + public void setTime(int parameterIndex, Time x) throws SQLException { + rawStatement.setTime(parameterIndex, x); + } + + @Override + public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException { + rawStatement.setTimestamp(parameterIndex, x); + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException { + rawStatement.setAsciiStream(parameterIndex, x, length); + } + + @Override + @Deprecated + public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException { + rawStatement.setUnicodeStream(parameterIndex, x, length); + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException { + rawStatement.setBinaryStream(parameterIndex, x, length); + } + + @Override + public void clearParameters() throws SQLException { + rawStatement.clearParameters(); + } + + @Override + public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException { + rawStatement.setObject(parameterIndex, x, targetSqlType, targetSqlType); + } + + @Override + public void setObject(int parameterIndex, Object x) throws SQLException { + rawStatement.setObject(parameterIndex, x); + } + + @Override + public boolean execute() throws SQLException { + return rawStatement.execute(); + } + + @Override + public void addBatch() throws SQLException { + rawStatement.addBatch(); + } + + @Override + public void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException { + rawStatement.setCharacterStream(parameterIndex, reader, length); + } + + @Override + public void setRef(int parameterIndex, Ref x) throws SQLException { + rawStatement.setRef(parameterIndex, x); + } + + @Override + public void setBlob(int parameterIndex, Blob x) throws SQLException { + rawStatement.setBlob(parameterIndex, x); + } + + @Override + public void setClob(int parameterIndex, Clob x) throws SQLException { + rawStatement.setClob(parameterIndex, x); + } + + @Override + public void setArray(int parameterIndex, Array x) throws SQLException { + rawStatement.setArray(parameterIndex, x); + } + + @Override + public ResultSetMetaData getMetaData() throws SQLException { + return rawStatement.getMetaData(); + } + + @Override + public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException { + rawStatement.setDate(parameterIndex, x, cal); + } + + @Override + public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException { + rawStatement.setTime(parameterIndex, x, cal); + } + + @Override + public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException { + rawStatement.setTimestamp(parameterIndex, x, cal); + } + + @Override + public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException { + rawStatement.setNull(parameterIndex, sqlType, typeName); + } + + @Override + public void setURL(int parameterIndex, URL x) throws SQLException { + rawStatement.setURL(parameterIndex, x); + } + + @Override + public ParameterMetaData getParameterMetaData() throws SQLException { + return rawStatement.getParameterMetaData(); + } + + @Override + public void setRowId(int parameterIndex, RowId x) throws SQLException { + rawStatement.setRowId(parameterIndex, x); + } + + @Override + public void setNString(int parameterIndex, String value) throws SQLException { + rawStatement.setNString(parameterIndex, value); + } + + @Override + public void setNCharacterStream(int parameterIndex, Reader value, long length) throws SQLException { + rawStatement.setCharacterStream(parameterIndex, value, length); + } + + @Override + public void setNClob(int parameterIndex, NClob value) throws SQLException { + rawStatement.setNClob(parameterIndex, value); + } + + @Override + public void setClob(int parameterIndex, Reader reader, long length) throws SQLException { + rawStatement.setClob(parameterIndex, reader, length); + } + + @Override + public void setBlob(int parameterIndex, InputStream inputStream, long length) throws SQLException { + rawStatement.setBlob(parameterIndex, inputStream, length); + } + + @Override + public void setNClob(int parameterIndex, Reader reader, long length) throws SQLException { + rawStatement.setNClob(parameterIndex, reader, length); + } + + @Override + public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException { + rawStatement.setSQLXML(parameterIndex, xmlObject); + } + + @Override + public void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) throws SQLException { + rawStatement.setObject(parameterIndex, x, targetSqlType, scaleOrLength); + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException { + rawStatement.setAsciiStream(parameterIndex, x, length); + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException { + rawStatement.setBinaryStream(parameterIndex, x, length); + } + + @Override + public void setCharacterStream(int parameterIndex, Reader reader, long length) throws SQLException { + rawStatement.setCharacterStream(parameterIndex, reader, length); + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException { + rawStatement.setAsciiStream(parameterIndex, x); + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException { + rawStatement.setBinaryStream(parameterIndex, x); + } + + @Override + public void setCharacterStream(int parameterIndex, Reader reader) throws SQLException { + rawStatement.setCharacterStream(parameterIndex, reader); + } + + @Override + public void setNCharacterStream(int parameterIndex, Reader value) throws SQLException { + rawStatement.setNCharacterStream(parameterIndex, value); + } + + @Override + public void setClob(int parameterIndex, Reader reader) throws SQLException { + rawStatement.setClob(parameterIndex, reader); + } + + @Override + public void setBlob(int parameterIndex, InputStream inputStream) throws SQLException { + rawStatement.setBlob(parameterIndex, inputStream); + } + + @Override + public void setNClob(int parameterIndex, Reader reader) throws SQLException { + rawStatement.setNClob(parameterIndex, reader); + } + +} diff --git a/hutool-db/src/main/java/cn/hutool/db/sql/Wrapper.java b/hutool-db/src/main/java/cn/hutool/db/sql/Wrapper.java new file mode 100644 index 000000000..856840f71 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/sql/Wrapper.java @@ -0,0 +1,189 @@ +package cn.hutool.db.sql; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Map.Entry; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Editor; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.db.Entity; + +/** + * 包装器
+ * 主要用于字段名的包装(在字段名的前后加字符,例如反引号来避免与数据库的关键字冲突) + * @author Looly + * + */ +public class Wrapper { + + /** 前置包装符号 */ + private Character preWrapQuote; + /** 后置包装符号 */ + private Character sufWrapQuote; + + public Wrapper() { + } + + /** + * 构造 + * @param wrapQuote 单包装字符 + */ + public Wrapper(Character wrapQuote) { + this.preWrapQuote = wrapQuote; + this.sufWrapQuote = wrapQuote; + } + + /** + * 包装符号 + * @param preWrapQuote 前置包装符号 + * @param sufWrapQuote 后置包装符号 + */ + public Wrapper(Character preWrapQuote, Character sufWrapQuote) { + this.preWrapQuote = preWrapQuote; + this.sufWrapQuote = sufWrapQuote; + } + + //--------------------------------------------------------------- Getters and Setters start + /** + * @return 前置包装符号 + */ + public char getPreWrapQuote() { + return preWrapQuote; + } + /** + * 设置前置包装的符号 + * @param preWrapQuote 前置包装符号 + */ + public void setPreWrapQuote(Character preWrapQuote) { + this.preWrapQuote = preWrapQuote; + } + + /** + * @return 后置包装符号 + */ + public char getSufWrapQuote() { + return sufWrapQuote; + } + /** + * 设置后置包装的符号 + * @param sufWrapQuote 后置包装符号 + */ + public void setSufWrapQuote(Character sufWrapQuote) { + this.sufWrapQuote = sufWrapQuote; + } + //--------------------------------------------------------------- Getters and Setters end + + /** + * 包装字段名
+ * 有时字段与SQL的某些关键字冲突,导致SQL出错,因此需要将字段名用单引号或者反引号包装起来,避免冲突 + * @param field 字段名 + * @return 包装后的字段名 + */ + public String wrap(String field){ + if(preWrapQuote == null || sufWrapQuote == null || StrUtil.isBlank(field)) { + return field; + } + + //如果已经包含包装的引号,返回原字符 + if(StrUtil.isSurround(field, preWrapQuote, sufWrapQuote)){ + return field; + } + + //如果字段中包含通配符或者括号(字段通配符或者函数),不做包装 + if(StrUtil.containsAnyIgnoreCase(field, "*", "(", " ", "as")) { + return field; + } + + //对于Oracle这类数据库,表名中包含用户名需要单独拆分包装 + if(field.contains(StrUtil.DOT)){ + final Collection target = CollectionUtil.filter(StrUtil.split(field, StrUtil.C_DOT), new Editor(){ + @Override + public String edit(String t) { + return StrUtil.format("{}{}{}", preWrapQuote, t, sufWrapQuote); + } + }); + return CollectionUtil.join(target, StrUtil.DOT); + } + + return StrUtil.format("{}{}{}", preWrapQuote, field, sufWrapQuote); + } + + /** + * 包装字段名
+ * 有时字段与SQL的某些关键字冲突,导致SQL出错,因此需要将字段名用单引号或者反引号包装起来,避免冲突 + * @param fields 字段名 + * @return 包装后的字段名 + */ + public String[] wrap(String... fields){ + if(ArrayUtil.isEmpty(fields)) { + return fields; + } + + String[] wrappedFields = new String[fields.length]; + for(int i = 0; i < fields.length; i++) { + wrappedFields[i] = wrap(fields[i]); + } + + return wrappedFields; + } + + /** + * 包装字段名
+ * 有时字段与SQL的某些关键字冲突,导致SQL出错,因此需要将字段名用单引号或者反引号包装起来,避免冲突 + * @param fields 字段名 + * @return 包装后的字段名 + */ + public Collection wrap(Collection fields){ + if(CollectionUtil.isEmpty(fields)) { + return fields; + } + + return Arrays.asList(wrap(fields.toArray(new String[fields.size()]))); + } + + /** + * 包装字段名
+ * 有时字段与SQL的某些关键字冲突,导致SQL出错,因此需要将字段名用单引号或者反引号包装起来,避免冲突 + * @param entity 被包装的实体 + * @return 包装后的字段名 + */ + public Entity wrap(Entity entity){ + if(null == entity) { + return null; + } + + final Entity wrapedEntity = new Entity(); + + //wrap table name + wrapedEntity.setTableName(wrap(entity.getTableName())); + + //wrap fields + for (Entry entry : entity.entrySet()) { + wrapedEntity.set(wrap(entry.getKey()), entry.getValue()); + } + + return wrapedEntity; + } + + /** + * 包装字段名
+ * 有时字段与SQL的某些关键字冲突,导致SQL出错,因此需要将字段名用单引号或者反引号包装起来,避免冲突 + * @param conditions 被包装的实体 + * @return 包装后的字段名 + */ + public Condition[] wrap(Condition... conditions){ + final Condition[] clonedConditions = new Condition[conditions.length]; + if(ArrayUtil.isNotEmpty(conditions)) { + Condition clonedCondition; + for(int i = 0; i < conditions.length; i++) { + clonedCondition = conditions[i].clone(); + clonedCondition.setField(wrap(clonedCondition.getField())); + clonedConditions[i] = clonedCondition; + } + } + + return clonedConditions; + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/sql/package-info.java b/hutool-db/src/main/java/cn/hutool/db/sql/package-info.java new file mode 100644 index 000000000..e5c158eba --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/sql/package-info.java @@ -0,0 +1,7 @@ +/** + * SQL语句和Statement构建封装 + * + * @author looly + * + */ +package cn.hutool.db.sql; \ No newline at end of file diff --git a/hutool-db/src/main/java/cn/hutool/db/transaction/TransactionLevel.java b/hutool-db/src/main/java/cn/hutool/db/transaction/TransactionLevel.java new file mode 100644 index 000000000..cd501f84a --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/transaction/TransactionLevel.java @@ -0,0 +1,75 @@ +package cn.hutool.db.transaction; + +import java.sql.Connection; + +/** + * 事务级别枚举 + * + *

+ * 脏读(Dirty Read)
+ * 一个事务会读到另一个事务更新后但未提交的数据,如果另一个事务回滚,那么当前事务读到的数据就是脏数据 + *

+ * 不可重复读(Non Repeatable Read)
+ * 在一个事务内,多次读同一数据,在这个事务还没有结束时,如果另一个事务恰好修改了这个数据,那么,在第一个事务中,两次读取的数据就可能不一致 + *

+ * 幻读(Phantom Read)
+ * 在一个事务中,第一次查询某条记录,发现没有,但是,当试图更新这条不存在的记录时,竟然能成功,且可以再次读取同一条记录。 + * + * @see Connection#TRANSACTION_NONE + * @see Connection#TRANSACTION_READ_UNCOMMITTED + * @see Connection#TRANSACTION_READ_COMMITTED + * @see Connection#TRANSACTION_REPEATABLE_READ + * @see Connection#TRANSACTION_SERIALIZABLE + * @author looly + * @since 4.1.2 + */ +public enum TransactionLevel { + /** 驱动不支持事务 */ + NONE(Connection.TRANSACTION_NONE), + + /** + * 允许脏读、不可重复读和幻读 + *

+ * 在这种隔离级别下,一个事务会读到另一个事务更新后但未提交的数据,如果另一个事务回滚,那么当前事务读到的数据就是脏数据,这就是脏读(Dirty Read) + */ + READ_UNCOMMITTED(Connection.TRANSACTION_READ_UNCOMMITTED), + + /** + * 禁止脏读,但允许不可重复读和幻读 + *

+ * 此级别下,一个事务可能会遇到不可重复读(Non Repeatable Read)的问题。
+ * 不可重复读是指,在一个事务内,多次读同一数据,在这个事务还没有结束时,如果另一个事务恰好修改了这个数据,那么,在第一个事务中,两次读取的数据就可能不一致。 + */ + READ_COMMITTED(Connection.TRANSACTION_READ_COMMITTED), + + /** + * 禁止脏读和不可重复读,但允许幻读,MySQL的InnoDB引擎默认使用此隔离级别。 + *

+ * 此级别下,一个事务可能会遇到幻读(Phantom Read)的问题。
+ * 幻读是指,在一个事务中,第一次查询某条记录,发现没有,但是,当试图更新这条不存在的记录时,可以成功,且可以再次读取同一条记录。 + */ + REPEATABLE_READ(Connection.TRANSACTION_REPEATABLE_READ), + + /** + * 禁止脏读、不可重复读和幻读 + *

+ * 虽然Serializable隔离级别下的事务具有最高的安全性,但是,由于事务是串行执行,所以效率会大大下降,应用程序的性能会急剧降低。 + */ + SERIALIZABLE(Connection.TRANSACTION_SERIALIZABLE); + + /** 事务级别,对应Connection中的常量值 */ + private int level; + + private TransactionLevel(int level) { + this.level = level; + } + + /** + * 获取数据库事务级别int值 + * + * @return 数据库事务级别int值 + */ + public int getLevel() { + return this.level; + } +} diff --git a/hutool-db/src/main/java/cn/hutool/db/transaction/package-info.java b/hutool-db/src/main/java/cn/hutool/db/transaction/package-info.java new file mode 100644 index 000000000..4686377c5 --- /dev/null +++ b/hutool-db/src/main/java/cn/hutool/db/transaction/package-info.java @@ -0,0 +1,7 @@ +/** + * 事务相关类和操作 + * + * @author looly + * + */ +package cn.hutool.db.transaction; \ No newline at end of file diff --git a/hutool-db/src/test/java/cn/hutool/db/CRUDTest.java b/hutool-db/src/test/java/cn/hutool/db/CRUDTest.java new file mode 100644 index 000000000..e06e89331 --- /dev/null +++ b/hutool-db/src/test/java/cn/hutool/db/CRUDTest.java @@ -0,0 +1,187 @@ +package cn.hutool.db; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.SQLException; +import java.util.List; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Console; +import cn.hutool.db.ActiveEntity; +import cn.hutool.db.Db; +import cn.hutool.db.Entity; +import cn.hutool.db.handler.EntityListHandler; +import cn.hutool.db.pojo.User; +import cn.hutool.db.sql.Condition; +import cn.hutool.db.sql.Condition.LikeType; + +/** + * 增删改查测试 + * + * @author looly + * + */ +public class CRUDTest { + + private static Db db = Db.use("test"); + + @Test + public void findIsNullTest() throws SQLException { + List results = db.findAll(Entity.create("user").set("age", "is null")); + Assert.assertEquals(0, results.size()); + } + + @Test + public void findIsNullTest2() throws SQLException { + List results = db.findAll(Entity.create("user").set("age", "= null")); + Assert.assertEquals(0, results.size()); + } + + @Test + public void findIsNullTest3() throws SQLException { + List results = db.findAll(Entity.create("user").set("age", null)); + Assert.assertEquals(0, results.size()); + } + + @Test + public void findBetweenTest() throws SQLException { + List results = db.findAll(Entity.create("user").set("age", "between '18' and '40'")); + Assert.assertEquals(1, results.size()); + } + + @Test + public void findByBigIntegerTest() throws SQLException { + List results = db.findAll(Entity.create("user").set("age", new BigInteger("12"))); + Assert.assertEquals(2, results.size()); + } + + @Test + public void findByBigDecimalTest() throws SQLException { + List results = db.findAll(Entity.create("user").set("age", new BigDecimal("12"))); + Assert.assertEquals(2, results.size()); + } + + @Test + public void findLikeTest() throws SQLException { + List results = db.findAll(Entity.create("user").set("name", "like \"%三%\"")); + Assert.assertEquals(2, results.size()); + } + + @Test + public void findLikeTest2() throws SQLException { + List results = db.findAll(Entity.create("user").set("name", new Condition("name", "三", LikeType.Contains))); + Assert.assertEquals(2, results.size()); + } + + @Test + public void findLikeTest3() throws SQLException { + List results = db.findAll(Entity.create("user").set("name", new Condition("name", null, LikeType.Contains))); + Assert.assertEquals(0, results.size()); + } + + @Test + public void findInTest() throws SQLException { + List results = db.findAll(Entity.create("user").set("id", "in 1,2,3")); + Assert.assertEquals(2, results.size()); + } + + @Test + public void findInTest2() throws SQLException { + List results = db.findAll(Entity.create("user").set("id", new Condition("id", new long[] {1,2,3}))); + Assert.assertEquals(2, results.size()); + } + + @Test + public void findAllTest() throws SQLException { + List results = db.findAll("user"); + Assert.assertEquals(4, results.size()); + } + + @Test + public void findTest() throws SQLException { + List find = db.find(CollUtil.newArrayList("name AS name2"), Entity.create("user"), new EntityListHandler()); + Assert.assertFalse(find.isEmpty()); + } + + @Test + public void findActiveTest() throws SQLException { + ActiveEntity entity = new ActiveEntity(db, "user"); + entity.setFieldNames("name AS name2").load(); + Assert.assertEquals("user", entity.getTableName()); + Assert.assertFalse(entity.isEmpty()); + } + + /** + * 对增删改查做单元测试 + * + * @throws SQLException + */ + @Test + @Ignore + public void crudTest() throws SQLException { + + // 增 + Long id = db.insertForGeneratedKey(Entity.create("user").set("name", "unitTestUser").set("age", 66)); + Assert.assertTrue(id > 0); + Entity result = db.get("user", "name", "unitTestUser"); + Assert.assertSame(66, (int) result.getInt("age")); + + // 改 + int update = db.update(Entity.create().set("age", 88), Entity.create("user").set("name", "unitTestUser")); + Assert.assertTrue(update > 0); + Entity result2 = db.get("user", "name", "unitTestUser"); + Assert.assertSame(88, (int) result2.getInt("age")); + + // 删 + int del = db.del("user", "name", "unitTestUser"); + Assert.assertTrue(del > 0); + Entity result3 = db.get("user", "name", "unitTestUser"); + Assert.assertNull(result3); + } + + @Test + @Ignore + public void insertBatchTest() throws SQLException { + User user1 = new User(); + user1.setName("张三"); + user1.setAge(12); + user1.setBirthday("19900112"); + user1.setGender(true); + + User user2 = new User(); + user2.setName("李四"); + user2.setAge(12); + user2.setBirthday("19890512"); + user2.setGender(false); + + Entity data1 = Entity.parse(user1); + Entity data2 = Entity.parse(user2); + + Console.log(data1); + Console.log(data2); + + int[] result = db.insert(CollUtil.newArrayList(data1, data2)); + Console.log(result); + } + + @Test + @Ignore + public void insertBatchOneTest() throws SQLException { + User user1 = new User(); + user1.setName("张三"); + user1.setAge(12); + user1.setBirthday("19900112"); + user1.setGender(true); + + Entity data1 = Entity.parse(user1); + + Console.log(data1); + + int[] result = db.insert(CollUtil.newArrayList(data1)); + Console.log(result); + } +} diff --git a/hutool-db/src/test/java/cn/hutool/db/ConcurentTest.java b/hutool-db/src/test/java/cn/hutool/db/ConcurentTest.java new file mode 100644 index 000000000..9cb8755c2 --- /dev/null +++ b/hutool-db/src/test/java/cn/hutool/db/ConcurentTest.java @@ -0,0 +1,54 @@ +package cn.hutool.db; + +import java.sql.SQLException; +import java.util.List; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Console; +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.db.Db; +import cn.hutool.db.DbRuntimeException; +import cn.hutool.db.Entity; +import cn.hutool.db.handler.EntityListHandler; + +/** + * SqlRunner线程安全测试 + * + * @author looly + * + */ +@Ignore +public class ConcurentTest { + + private Db db; + + @Before + public void init() { + db = Db.use("test"); + } + + @Test + public void findTest() { + for(int i = 0; i < 10000; i++) { + ThreadUtil.execute(new Runnable() { + @Override + public void run() { + List find = null; + try { + find = db.find(CollectionUtil.newArrayList("name AS name2"), Entity.create("user"), new EntityListHandler()); + } catch (SQLException e) { + throw new DbRuntimeException(e); + } + Console.log(find); + } + }); + } + + //主线程关闭会导致连接池销毁,sleep避免此情况引起的问题 + ThreadUtil.sleep(5000); + } +} diff --git a/hutool-db/src/test/java/cn/hutool/db/DbTest.java b/hutool-db/src/test/java/cn/hutool/db/DbTest.java new file mode 100644 index 000000000..c629c73c2 --- /dev/null +++ b/hutool-db/src/test/java/cn/hutool/db/DbTest.java @@ -0,0 +1,56 @@ +package cn.hutool.db; + +import java.sql.SQLException; +import java.util.List; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.lang.func.VoidFunc1; +import cn.hutool.db.sql.Condition; +import cn.hutool.log.StaticLog; + +/** + * Db对象单元测试 + * @author looly + * + */ +public class DbTest { + + @Test + public void findTest() throws SQLException { + Db.use(); + + List find = Db.use().find(Entity.create("user").set("age", 18)); + Assert.assertEquals("王五", find.get(0).get("name")); + } + + @Test + public void findByTest() throws SQLException { + Db.use(); + + List find = Db.use().findBy("user", + Condition.parse("age", "> 18"), + Condition.parse("age", "< 100") + ); + for (Entity entity : find) { + StaticLog.debug("{}", entity); + } + Assert.assertEquals("unitTestUser", find.get(0).get("name")); + } + + @Test + @Ignore + public void txTest() throws SQLException { + Db.use().tx(new VoidFunc1() { + + @Override + public void call(Db db) throws SQLException { + db.insert(Entity.create("user").set("name", "unitTestUser2")); + db.update(Entity.create().set("age", 79), Entity.create("user").set("name", "unitTestUser2")); + db.del("user", "name", "unitTestUser2"); + } + }); + } +} diff --git a/hutool-db/src/test/java/cn/hutool/db/DsTest.java b/hutool-db/src/test/java/cn/hutool/db/DsTest.java new file mode 100644 index 000000000..4c0fca135 --- /dev/null +++ b/hutool-db/src/test/java/cn/hutool/db/DsTest.java @@ -0,0 +1,92 @@ +package cn.hutool.db; + +import java.sql.SQLException; +import java.util.List; + +import javax.sql.DataSource; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.db.Db; +import cn.hutool.db.Entity; +import cn.hutool.db.ds.DSFactory; +import cn.hutool.db.ds.c3p0.C3p0DSFactory; +import cn.hutool.db.ds.dbcp.DbcpDSFactory; +import cn.hutool.db.ds.druid.DruidDSFactory; +import cn.hutool.db.ds.hikari.HikariDSFactory; +import cn.hutool.db.ds.pooled.PooledDSFactory; +import cn.hutool.db.ds.tomcat.TomcatDSFactory; + +/** + * 数据源单元测试 + * + * @author Looly + * + */ +public class DsTest { + + @Test + public void defaultDsTest() throws SQLException { + DataSource ds = DSFactory.get("test"); + Db db = Db.use(ds); + List all = db.findAll("user"); + Assert.assertTrue(CollUtil.isNotEmpty(all)); + } + + @Test + public void hikariDsTest() throws SQLException { + DSFactory.setCurrentDSFactory(new HikariDSFactory()); + DataSource ds = DSFactory.get("test"); + Db db = Db.use(ds); + List all = db.findAll("user"); + Assert.assertTrue(CollUtil.isNotEmpty(all)); + } + + @Test + public void druidDsTest() throws SQLException { + DSFactory.setCurrentDSFactory(new DruidDSFactory()); + DataSource ds = DSFactory.get("test"); + + Db db = Db.use(ds); + List all = db.findAll("user"); + Assert.assertTrue(CollUtil.isNotEmpty(all)); + } + + @Test + public void tomcatDsTest() throws SQLException { + DSFactory.setCurrentDSFactory(new TomcatDSFactory()); + DataSource ds = DSFactory.get("test"); + Db db = Db.use(ds); + List all = db.findAll("user"); + Assert.assertTrue(CollUtil.isNotEmpty(all)); + } + + @Test + public void dbcpDsTest() throws SQLException { + DSFactory.setCurrentDSFactory(new DbcpDSFactory()); + DataSource ds = DSFactory.get("test"); + Db db = Db.use(ds); + List all = db.findAll("user"); + Assert.assertTrue(CollUtil.isNotEmpty(all)); + } + + @Test + public void c3p0DsTest() throws SQLException { + DSFactory.setCurrentDSFactory(new C3p0DSFactory()); + DataSource ds = DSFactory.get("test"); + Db db = Db.use(ds); + List all = db.findAll("user"); + Assert.assertTrue(CollUtil.isNotEmpty(all)); + } + + @Test + public void hutoolPoolTest() throws SQLException { + DSFactory.setCurrentDSFactory(new PooledDSFactory()); + DataSource ds = DSFactory.get("test"); + Db db = Db.use(ds); + List all = db.findAll("user"); + Assert.assertTrue(CollUtil.isNotEmpty(all)); + } +} diff --git a/hutool-db/src/test/java/cn/hutool/db/EntityTest.java b/hutool-db/src/test/java/cn/hutool/db/EntityTest.java new file mode 100644 index 000000000..5a8ea6d5b --- /dev/null +++ b/hutool-db/src/test/java/cn/hutool/db/EntityTest.java @@ -0,0 +1,48 @@ +package cn.hutool.db; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.db.Entity; +import cn.hutool.db.pojo.User; + +/** + * Entity测试 + * + * @author looly + * + */ +public class EntityTest { + + @Test + public void parseTest() { + User user = new User(); + user.setId(1); + user.setName("test"); + + Entity entity = Entity.create("testTable").parseBean(user); + Assert.assertEquals(Integer.valueOf(1), entity.getInt("id")); + Assert.assertEquals("test", entity.getStr("name")); + } + + @Test + public void parseTest2() { + User user = new User(); + user.setId(1); + user.setName("test"); + + Entity entity = Entity.create().parseBean(user); + Assert.assertEquals(Integer.valueOf(1), entity.getInt("id")); + Assert.assertEquals("test", entity.getStr("name")); + Assert.assertEquals("user", entity.getTableName()); + } + + @Test + public void entityToBeanIgnoreCaseTest() { + Entity entity = Entity.create().set("ID", 2).set("NAME", "testName"); + User user = entity.toBeanIgnoreCase(User.class); + + Assert.assertEquals(Integer.valueOf(2), user.getId()); + Assert.assertEquals("testName", user.getName()); + } +} diff --git a/hutool-db/src/test/java/cn/hutool/db/FindBeanTest.java b/hutool-db/src/test/java/cn/hutool/db/FindBeanTest.java new file mode 100644 index 000000000..973bfef6b --- /dev/null +++ b/hutool-db/src/test/java/cn/hutool/db/FindBeanTest.java @@ -0,0 +1,71 @@ +package cn.hutool.db; + +import java.sql.SQLException; +import java.util.List; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import cn.hutool.db.Db; +import cn.hutool.db.Entity; +import cn.hutool.db.pojo.User; + +/** + * Entity测试 + * + * @author looly + * + */ +public class FindBeanTest { + + Db db; + + @Before + public void init() { + db = Db.use("test"); + } + + @Test + public void findAllBeanTest() throws SQLException { + List results = db.findAll(Entity.create("user"), User.class); + + Assert.assertEquals(4, results.size()); + Assert.assertEquals(Integer.valueOf(1), results.get(0).getId()); + Assert.assertEquals("张三", results.get(0).getName()); + } + + @Test + @SuppressWarnings("rawtypes") + public void findAllListTest() throws SQLException { + List results = db.findAll(Entity.create("user"), List.class); + + Assert.assertEquals(4, results.size()); + Assert.assertEquals(1, results.get(0).get(0)); + Assert.assertEquals("张三", results.get(0).get(1)); + } + + @Test + public void findAllArrayTest() throws SQLException { + List results = db.findAll(Entity.create("user"), Object[].class); + + Assert.assertEquals(4, results.size()); + Assert.assertEquals(1, results.get(0)[0]); + Assert.assertEquals("张三", results.get(0)[1]); + } + + @Test + public void findAllStringTest() throws SQLException { + List results = db.findAll(Entity.create("user"), String.class); + Assert.assertEquals(4, results.size()); + } + + @Test + public void findAllStringArrayTest() throws SQLException { + List results = db.findAll(Entity.create("user"), String[].class); + + Assert.assertEquals(4, results.size()); + Assert.assertEquals("1", results.get(0)[0]); + Assert.assertEquals("张三", results.get(0)[1]); + } +} diff --git a/hutool-db/src/test/java/cn/hutool/db/HsqldbTest.java b/hutool-db/src/test/java/cn/hutool/db/HsqldbTest.java new file mode 100644 index 000000000..e5c607c06 --- /dev/null +++ b/hutool-db/src/test/java/cn/hutool/db/HsqldbTest.java @@ -0,0 +1,38 @@ +package cn.hutool.db; + +import java.sql.SQLException; +import java.util.List; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import cn.hutool.db.Db; +import cn.hutool.db.Entity; + +/** + * HSQLDB数据库单元测试 + * + * @author looly + * + */ +public class HsqldbTest { + + private static final String DS_GROUP_NAME = "hsqldb"; + + @Before + public void init() throws SQLException { + Db db = Db.use(DS_GROUP_NAME); + db.execute("CREATE TABLE test(a INTEGER, b BIGINT)"); + db.insert(Entity.create("test").set("a", 1).set("b", 11)); + db.insert(Entity.create("test").set("a", 2).set("b", 21)); + db.insert(Entity.create("test").set("a", 3).set("b", 31)); + db.insert(Entity.create("test").set("a", 4).set("b", 41)); + } + + @Test + public void connTest() throws SQLException { + List query = Db.use(DS_GROUP_NAME).query("select * from test"); + Assert.assertEquals(4, query.size()); + } +} diff --git a/hutool-db/src/test/java/cn/hutool/db/MySQLTest.java b/hutool-db/src/test/java/cn/hutool/db/MySQLTest.java new file mode 100644 index 000000000..fd6aa2a26 --- /dev/null +++ b/hutool-db/src/test/java/cn/hutool/db/MySQLTest.java @@ -0,0 +1,65 @@ +package cn.hutool.db; + +import java.sql.SQLException; + +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.lang.Console; +import cn.hutool.core.lang.func.VoidFunc1; + +/** + * MySQL操作单元测试 + * + * @author looly + * + */ +public class MySQLTest { + + @Test + @Ignore + public void insertTest() throws SQLException { + for (int id = 100; id < 200; id++) { + Db.use("mysql").insert(Entity.create("user")// + .set("id", id)// + .set("name", "测试用户" + id)// + .set("text", "描述" + id)// + .set("test1", "t" + id)// + ); + } + } + + /** + * 事务测试
+ * 更新三条信息,低2条后抛出异常,正常情况下三条都应该不变 + * + * @throws SQLException + */ + @Test(expected=SQLException.class) + @Ignore + public void txTest() throws SQLException { + Db.use("mysql").tx(new VoidFunc1() { + + @Override + public void call(Db db) throws Exception { + int update = db.update(Entity.create("user").set("text", "描述100"), Entity.create().set("id", 100)); + db.update(Entity.create("user").set("text", "描述101"), Entity.create().set("id", 101)); + if(1 == update) { + // 手动指定异常,然后测试回滚触发 + throw new RuntimeException("Error"); + } + db.update(Entity.create("user").set("text", "描述102"), Entity.create().set("id", 102)); + } + }); + } + + @Test + @Ignore + public void pageTest() throws SQLException { + PageResult result = Db.use("mysql").page(Entity.create("user"), new Page(2, 10)); + for (Entity entity : result) { + Console.log(entity.get("id")); + } + } + +} diff --git a/hutool-db/src/test/java/cn/hutool/db/NamedSqlTest.java b/hutool-db/src/test/java/cn/hutool/db/NamedSqlTest.java new file mode 100644 index 000000000..78d01ae55 --- /dev/null +++ b/hutool-db/src/test/java/cn/hutool/db/NamedSqlTest.java @@ -0,0 +1,39 @@ +package cn.hutool.db; + +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.db.sql.NamedSql; + +public class NamedSqlTest { + + @Test + public void parseTest() { + String sql = "select * from table where id=@id and name = @name1 and nickName = :subName"; + + Map paramMap = MapUtil.builder("name1", (Object)"张三").put("age", 12).put("subName", "小豆豆").build(); + + NamedSql namedSql = new NamedSql(sql, paramMap); + //未指定参数原样输出 + Assert.assertEquals("select * from table where id=@id and name = ? and nickName = ?", namedSql.getSql()); + Assert.assertEquals("张三", namedSql.getParams()[0]); + Assert.assertEquals("小豆豆", namedSql.getParams()[1]); + } + + @Test + public void parseTest2() { + String sql = "select * from table where id=@id and name = @name1 and nickName = :subName"; + + Map paramMap = MapUtil.builder("name1", (Object)"张三").put("age", 12).put("subName", "小豆豆").put("id", null).build(); + + NamedSql namedSql = new NamedSql(sql, paramMap); + Assert.assertEquals("select * from table where id=? and name = ? and nickName = ?", namedSql.getSql()); + //指定了null参数的依旧替换,参数值为null + Assert.assertNull(namedSql.getParams()[0]); + Assert.assertEquals("张三", namedSql.getParams()[1]); + Assert.assertEquals("小豆豆", namedSql.getParams()[2]); + } +} diff --git a/hutool-db/src/test/java/cn/hutool/db/OracleTest.java b/hutool-db/src/test/java/cn/hutool/db/OracleTest.java new file mode 100644 index 000000000..57c6e584f --- /dev/null +++ b/hutool-db/src/test/java/cn/hutool/db/OracleTest.java @@ -0,0 +1,69 @@ +package cn.hutool.db; + +import java.sql.SQLException; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.lang.Console; +import cn.hutool.db.Db; +import cn.hutool.db.Entity; +import cn.hutool.db.Page; +import cn.hutool.db.PageResult; +import cn.hutool.db.sql.Query; +import cn.hutool.db.sql.SqlBuilder; +import cn.hutool.db.sql.SqlUtil; + +/** + * Oracle操作单元测试 + * + * @author looly + * + */ +public class OracleTest { + + @Test + public void oraclePageSqlTest() { + Page page = new Page(1, 10); + Entity where = Entity.create("PMCPERFORMANCEINFO").set("yearPI", "2017"); + final Query query = new Query(SqlUtil.buildConditions(where), where.getTableName()); + query.setPage(page); + + SqlBuilder find = SqlBuilder.create(null).query(query).orderBy(page.getOrders()); + final int[] startEnd = page.getStartEnd(); + SqlBuilder builder = SqlBuilder.create(null).append("SELECT * FROM ( SELECT row_.*, rownum rownum_ from ( ")// + .append(find)// + .append(" ) row_ where rownum <= ").append(startEnd[1])// + .append(") table_alias")// + .append(" where table_alias.rownum_ >= ").append(startEnd[0]);// + + String ok = "SELECT * FROM "// + + "( SELECT row_.*, rownum rownum_ from ( SELECT * FROM PMCPERFORMANCEINFO WHERE yearPI = ? ) row_ "// + + "where rownum <= 10) table_alias where table_alias.rownum_ >= 0";// + Assert.assertEquals(ok, builder.toString()); + } + + @Test + @Ignore + public void insertTest() throws SQLException { + for (int id = 100; id < 200; id++) { + Db.use("orcl").insert(Entity.create("T_USER")// + .set("ID", id)// + .set("name", "测试用户" + id)// + .set("TEXT", "描述" + id)// + .set("TEST1", "t" + id)// + ); + } + } + + @Test + @Ignore + public void pageTest() throws SQLException { + PageResult result = Db.use("orcl").page(Entity.create("T_USER"), new Page(2, 10)); + for (Entity entity : result) { + Console.log(entity.get("ID")); + } + } + +} diff --git a/hutool-db/src/test/java/cn/hutool/db/PostgreTest.java b/hutool-db/src/test/java/cn/hutool/db/PostgreTest.java new file mode 100644 index 000000000..874691e65 --- /dev/null +++ b/hutool-db/src/test/java/cn/hutool/db/PostgreTest.java @@ -0,0 +1,41 @@ +package cn.hutool.db; + +import java.sql.SQLException; + +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.lang.Console; +import cn.hutool.db.Db; +import cn.hutool.db.Entity; +import cn.hutool.db.Page; +import cn.hutool.db.PageResult; + +/** + * PostgreSQL 单元测试 + * + * @author looly + * + */ +public class PostgreTest { + + @Test + @Ignore + public void insertTest() throws SQLException { + for (int id = 100; id < 200; id++) { + Db.use("postgre").insert(Entity.create("user")// + .set("id", id)// + .set("name", "测试用户" + id)// + ); + } + } + + @Test + @Ignore + public void pageTest() throws SQLException { + PageResult result = Db.use("postgre").page(Entity.create("user"), new Page(2, 10)); + for (Entity entity : result) { + Console.log(entity.get("id")); + } + } +} diff --git a/hutool-db/src/test/java/cn/hutool/db/SessionTest.java b/hutool-db/src/test/java/cn/hutool/db/SessionTest.java new file mode 100644 index 000000000..978996edc --- /dev/null +++ b/hutool-db/src/test/java/cn/hutool/db/SessionTest.java @@ -0,0 +1,40 @@ +package cn.hutool.db; + +import java.sql.SQLException; + +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.lang.func.VoidFunc1; + +/** + * 事务性数据库操作单元测试 + * @author looly + * + */ +public class SessionTest { + + @Test + @Ignore + public void transTest() { + Session session = Session.create("test"); + try { + session.beginTransaction(); + session.update(Entity.create().set("age", 76), Entity.create("user").set("name", "unitTestUser")); + session.commit(); + } catch (SQLException e) { + session.quietRollback(); + } + } + + @Test + @Ignore + public void txTest() throws SQLException { + Session.create("test").tx(new VoidFunc1() { + @Override + public void call(Session session) throws SQLException { + session.update(Entity.create().set("age", 78), Entity.create("user").set("name", "unitTestUser")); + } + }); + } +} diff --git a/hutool-db/src/test/java/cn/hutool/db/SqlServerTest.java b/hutool-db/src/test/java/cn/hutool/db/SqlServerTest.java new file mode 100644 index 000000000..f37df941f --- /dev/null +++ b/hutool-db/src/test/java/cn/hutool/db/SqlServerTest.java @@ -0,0 +1,48 @@ +package cn.hutool.db; + +import java.sql.SQLException; + +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.lang.Console; +import cn.hutool.db.Db; +import cn.hutool.db.Entity; +import cn.hutool.db.Page; +import cn.hutool.db.PageResult; + +/** + * SQL Server操作单元测试 + * + * @author looly + * + */ +public class SqlServerTest { + + @Test + @Ignore + public void createTableTest() throws SQLException { + Db.use("sqlserver").execute("create table T_USER(ID bigint, name varchar(255))"); + } + + @Test + @Ignore + public void insertTest() throws SQLException { + for (int id = 100; id < 200; id++) { + Db.use("sqlserver").insert(Entity.create("T_USER")// + .set("ID", id)// + .set("name", "测试用户" + id)// + ); + } + } + + @Test + @Ignore + public void pageTest() throws SQLException { + PageResult result = Db.use("sqlserver").page(Entity.create("T_USER"), new Page(2, 10)); + for (Entity entity : result) { + Console.log(entity.get("ID")); + } + } + +} diff --git a/hutool-db/src/test/java/cn/hutool/db/UpdateTest.java b/hutool-db/src/test/java/cn/hutool/db/UpdateTest.java new file mode 100644 index 000000000..61036149a --- /dev/null +++ b/hutool-db/src/test/java/cn/hutool/db/UpdateTest.java @@ -0,0 +1,37 @@ +package cn.hutool.db; + +import java.sql.SQLException; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.db.Db; +import cn.hutool.db.Entity; + +public class UpdateTest { + + Db db; + + @Before + public void init() { + db = Db.use("test"); + } + + /** + * 对更新做单元测试 + * + * @throws SQLException + */ + @Test + @Ignore + public void updateTest() throws SQLException { + + // 改 + int update = db.update(Entity.create("user").set("age", 88), Entity.create().set("name", "unitTestUser")); + Assert.assertTrue(update > 0); + Entity result2 = db.get("user", "name", "unitTestUser"); + Assert.assertSame(88, (int) result2.getInt("age")); + } +} diff --git a/hutool-db/src/test/java/cn/hutool/db/meta/MetaUtilTest.java b/hutool-db/src/test/java/cn/hutool/db/meta/MetaUtilTest.java new file mode 100644 index 000000000..96bd36aec --- /dev/null +++ b/hutool-db/src/test/java/cn/hutool/db/meta/MetaUtilTest.java @@ -0,0 +1,40 @@ +package cn.hutool.db.meta; + +import java.util.List; + +import javax.sql.DataSource; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.db.ds.DSFactory; + +/** + * 元数据信息单元测试 + * + * @author Looly + * + */ +public class MetaUtilTest { + DataSource ds = DSFactory.get("test"); + + @Test + public void getTablesTest() { + List tables = MetaUtil.getTables(ds); + Assert.assertEquals("user", tables.get(0)); + } + + @Test + public void getTableMetaTest() { + Table table = MetaUtil.getTableMeta(ds, "user"); + Assert.assertEquals(CollectionUtil.newHashSet("id"), table.getPkNames()); + } + + @Test + public void getColumnNamesTest() { + String[] names = MetaUtil.getColumnNames(ds, "user"); + Assert.assertArrayEquals(StrUtil.splitToArray("id,name,age,birthday,gender", ','), names); + } +} diff --git a/hutool-db/src/test/java/cn/hutool/db/pojo/User.java b/hutool-db/src/test/java/cn/hutool/db/pojo/User.java new file mode 100644 index 000000000..c496259ca --- /dev/null +++ b/hutool-db/src/test/java/cn/hutool/db/pojo/User.java @@ -0,0 +1,60 @@ +package cn.hutool.db.pojo; + +/** + * 测试用POJO,与测试数据库中的user表对应 + * + * @author looly + * + */ +public class User { + private Integer id; + private String name; + private int age; + private String birthday; + private boolean gender; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public String getBirthday() { + return birthday; + } + + public void setBirthday(String birthday) { + this.birthday = birthday; + } + + public boolean isGender() { + return gender; + } + + public void setGender(boolean gender) { + this.gender = gender; + } + + @Override + public String toString() { + return "User [id=" + id + ", name=" + name + ", age=" + age + ", birthday=" + birthday + ", gender=" + gender + "]"; + } +} diff --git a/hutool-db/src/test/java/cn/hutool/db/sql/ConditionTest.java b/hutool-db/src/test/java/cn/hutool/db/sql/ConditionTest.java new file mode 100644 index 000000000..31bbfc53e --- /dev/null +++ b/hutool-db/src/test/java/cn/hutool/db/sql/ConditionTest.java @@ -0,0 +1,55 @@ +package cn.hutool.db.sql; + +import org.junit.Assert; +import org.junit.Test; + +public class ConditionTest { + + @Test + public void toStringTest() { + Condition conditionNull = new Condition("user", null); + Assert.assertEquals("user IS NULL", conditionNull.toString()); + + Condition conditionNotNull = new Condition("user", "!= null"); + Assert.assertEquals("user IS NOT NULL", conditionNotNull.toString()); + + Condition condition2 = new Condition("user", "= zhangsan"); + Assert.assertEquals("user = ?", condition2.toString()); + + Condition conditionLike = new Condition("user", "like %aaa"); + Assert.assertEquals("user LIKE ?", conditionLike.toString()); + + Condition conditionIn = new Condition("user", "in 1,2,3"); + Assert.assertEquals("user IN (?,?,?)", conditionIn.toString()); + + Condition conditionBetween = new Condition("user", "between 12 and 13"); + Assert.assertEquals("user BETWEEN ? AND ?", conditionBetween.toString()); + } + + @Test + public void toStringNoPlaceHolderTest() { + Condition conditionNull = new Condition("user", null); + conditionNull.setPlaceHolder(false); + Assert.assertEquals("user IS NULL", conditionNull.toString()); + + Condition conditionNotNull = new Condition("user", "!= null"); + conditionNotNull.setPlaceHolder(false); + Assert.assertEquals("user IS NOT NULL", conditionNotNull.toString()); + + Condition conditionEquals = new Condition("user", "= zhangsan"); + conditionEquals.setPlaceHolder(false); + Assert.assertEquals("user = zhangsan", conditionEquals.toString()); + + Condition conditionLike = new Condition("user", "like %aaa"); + conditionLike.setPlaceHolder(false); + Assert.assertEquals("user LIKE %aaa", conditionLike.toString()); + + Condition conditionIn = new Condition("user", "in 1,2,3"); + conditionIn.setPlaceHolder(false); + Assert.assertEquals("user IN (1,2,3)", conditionIn.toString()); + + Condition conditionBetween = new Condition("user", "between 12 and 13"); + conditionBetween.setPlaceHolder(false); + Assert.assertEquals("user BETWEEN 12 AND 13", conditionBetween.toString()); + } +} diff --git a/hutool-db/src/test/java/cn/hutool/db/sql/SqlBuilderTest.java b/hutool-db/src/test/java/cn/hutool/db/sql/SqlBuilderTest.java new file mode 100644 index 000000000..6f3027359 --- /dev/null +++ b/hutool-db/src/test/java/cn/hutool/db/sql/SqlBuilderTest.java @@ -0,0 +1,22 @@ +package cn.hutool.db.sql; + +import org.junit.Assert; +import org.junit.Test; + +public class SqlBuilderTest { + + @Test + public void queryNullTest() { + SqlBuilder builder = SqlBuilder.create().select().from("user").where(new Condition("name", "= null")); + Assert.assertEquals("SELECT * FROM user WHERE name IS NULL", builder.build()); + + SqlBuilder builder2 = SqlBuilder.create().select().from("user").where(new Condition("name", "is null")); + Assert.assertEquals("SELECT * FROM user WHERE name IS NULL", builder2.build()); + + SqlBuilder builder3 = SqlBuilder.create().select().from("user").where(LogicalOperator.AND, new Condition("name", "!= null")); + Assert.assertEquals("SELECT * FROM user WHERE name IS NOT NULL", builder3.build()); + + SqlBuilder builder4 = SqlBuilder.create().select().from("user").where(LogicalOperator.AND, new Condition("name", "is not null")); + Assert.assertEquals("SELECT * FROM user WHERE name IS NOT NULL", builder4.build()); + } +} diff --git a/hutool-db/src/test/resources/config/db.setting b/hutool-db/src/test/resources/config/db.setting new file mode 100644 index 000000000..775a6a521 --- /dev/null +++ b/hutool-db/src/test/resources/config/db.setting @@ -0,0 +1,51 @@ +#=================================================================== +# 数据库配置文件样例 +# DsFactory默认读取的配置文件是config/db.setting +# db.setting的配置包括两部分:基本连接信息和连接池配置信息。 +# 基本连接信息所有连接池都支持,连接池配置信息根据不同的连接池,连接池配置是根据连接池相应的配置项移植而来 +#=================================================================== + +## 打印SQL的配置 +# 是否在日志中显示执行的SQL,默认false +showSql = true +# 是否格式化显示的SQL,默认false +formatSql = true +# 是否显示SQL参数,默认false +showParams = true +# 打印SQL的日志等级,默认debug +sqlLevel = debug + +# 默认数据源 +url = jdbc:sqlite:test.db + + +# 测试数据源 +[test] +url = jdbc:sqlite:test.db + +# 测试用HSQLDB数据库 +[hsqldb] +url = jdbc:hsqldb:mem:mem_hutool +user = SA +pass = + +# 测试用Oracle数据库 +[orcl] +url = jdbc:oracle:thin:@//looly.centos:1521/XE +user = looly +pass = 123456 + +[mysql] +url = jdbc:mysql://looly.centos:3306/test_hutool?useSSL=false +user = root +pass = 123456 + +[postgre] +url = jdbc:postgresql://looly.centos:5432/test_hutool +user = postgres +pass = 123456 + +[sqlserver] +url = jdbc:sqlserver://looly.database.chinacloudapi.cn:1433;database=test;encrypt=true;trustServerCertificate=false;hostNameInCertificate=*.database.chinacloudapi.cn;loginTimeout=30; +user = looly@looly +pass = 123 \ No newline at end of file diff --git a/hutool-db/src/test/resources/config/example/db-example-c3p0.setting b/hutool-db/src/test/resources/config/example/db-example-c3p0.setting new file mode 100644 index 000000000..7fbadb196 --- /dev/null +++ b/hutool-db/src/test/resources/config/example/db-example-c3p0.setting @@ -0,0 +1,54 @@ +#=================================================================== +# 数据库配置文件样例 +# DsFactory默认读取的配置文件是config/db.setting +# db.setting的配置包括两部分:基本连接信息和连接池配置信息。 +# 基本连接信息所有连接池都支持,连接池配置信息根据不同的连接池,连接池配置是根据连接池相应的配置项移植而来 +#=================================================================== + +#---------------------------------------------------------------------------------------------------------------- +## 基本配置信息 +# JDBC URL,根据不同的数据库,使用相应的JDBC连接字符串 +url = jdbc:mysql://:/ +# 用户名,此处也可以使用 user 代替 +username = 用户名 +# 密码,此处也可以使用 pass 代替 +password = 密码 +# JDBC驱动名,可选(Hutool会自动识别) +driver = com.mysql.jdbc.Driver +# 是否在日志中显示执行的SQL +showSql = true +# 是否格式化显示的SQL +formatSql = true + +#---------------------------------------------------------------------------------------------------------------- +## 连接池配置项 + +## ---------------------------------------------------- C3P0 +# 连接池中保留的最大连接数。默认值: 15 +maxPoolSize = 15 +# 连接池中保留的最小连接数,默认为:3 +minPoolSize = 3 +# 初始化连接池中的连接数,取值应在minPoolSize与maxPoolSize之间,默认为3 +initialPoolSize = 3 +# 最大空闲时间,60秒内未使用则连接被丢弃。若为0则永不丢弃。默认值: 0 +maxIdleTime = 0 +# 当连接池连接耗尽时,客户端调用getConnection()后等待获取新连接的时间,超时后将抛出SQLException,如设为0则无限期等待。单位毫秒。默认: 0 +checkoutTimeout = 0 +# 当连接池中的连接耗尽的时候c3p0一次同时获取的连接数。默认值: 3 +acquireIncrement = 3 +# 定义在从数据库获取新连接失败后重复尝试的次数。默认值: 30 ;小于等于0表示无限次 +acquireRetryAttempts = 0 +# 重新尝试的时间间隔,默认为:1000毫秒 +acquireRetryDelay = 1000 +# 关闭连接时,是否提交未提交的事务,默认为false,即关闭连接,回滚未提交的事务 +autoCommitOnClose = false +# c3p0将建一张名为Test的空表,并使用其自带的查询语句进行测试。如果定义了这个参数那么属性preferredTestQuery将被忽略。你不能在这张Test表上进行任何操作,它将只供c3p0测试使用。默认值: null +automaticTestTable = null +# 如果为false,则获取连接失败将会引起所有等待连接池来获取连接的线程抛出异常,但是数据源仍有效保留,并在下次调用getConnection()的时候继续尝试获取连接。如果设为true,那么在尝试获取连接失败后该数据源将申明已断开并永久关闭。默认: false +breakAfterAcquireFailure = false +# 检查所有连接池中的空闲连接的检查频率。默认值: 0,不检查 +idleConnectionTestPeriod = 0 +# c3p0全局的PreparedStatements缓存的大小。如果maxStatements与maxStatementsPerConnection均为0,则缓存不生效,只要有一个不为0,则语句的缓存就能生效。如果默认值: 0 +maxStatements = 0 +# maxStatementsPerConnection定义了连接池内单个连接所拥有的最大缓存statements数。默认值: 0 +maxStatementsPerConnection = 0 \ No newline at end of file diff --git a/hutool-db/src/test/resources/config/example/db-example-dbcp.setting b/hutool-db/src/test/resources/config/example/db-example-dbcp.setting new file mode 100644 index 000000000..f5974defc --- /dev/null +++ b/hutool-db/src/test/resources/config/example/db-example-dbcp.setting @@ -0,0 +1,51 @@ +#=================================================================== +# 数据库配置文件样例 +# DsFactory默认读取的配置文件是config/db.setting +# db.setting的配置包括两部分:基本连接信息和连接池配置信息。 +# 基本连接信息所有连接池都支持,连接池配置信息根据不同的连接池,连接池配置是根据连接池相应的配置项移植而来 +#=================================================================== + +#---------------------------------------------------------------------------------------------------------------- +## 基本配置信息 +# JDBC URL,根据不同的数据库,使用相应的JDBC连接字符串 +url = jdbc:mysql://:/ +# 用户名,此处也可以使用 user 代替 +username = 用户名 +# 密码,此处也可以使用 pass 代替 +password = 密码 +# JDBC驱动名,可选(Hutool会自动识别) +driver = com.mysql.jdbc.Driver +# 是否在日志中显示执行的SQL +showSql = true +# 是否格式化显示的SQL +formatSql = true + + +#---------------------------------------------------------------------------------------------------------------- +## 连接池配置项 + +## ---------------------------------------------------- Dbcp +# (boolean) 连接池创建的连接的默认的auto-commit 状态 +defaultAutoCommit = true +# (boolean) 连接池创建的连接的默认的read-only 状态。 如果没有设置则setReadOnly 方法将不会被调用。 ( 某些驱动不支持只读模式, 比如:Informix) +defaultReadOnly = false +# (String) 连接池创建的连接的默认的TransactionIsolation 状态。 下面列表当中的某一个: ( 参考javadoc) NONE READ_COMMITTED EAD_UNCOMMITTED REPEATABLE_READ SERIALIZABLE +defaultTransactionIsolation = NONE +# (int) 初始化连接: 连接池启动时创建的初始化连接数量,1。2 版本后支持 +initialSize = 10 +# (int) 最大活动连接: 连接池在同一时间能够分配的最大活动连接的数量, 如果设置为非正数则表示不限制 +maxActive = 100 +# (int) 最大空闲连接: 连接池中容许保持空闲状态的最大连接数量, 超过的空闲连接将被释放, 如果设置为负数表示不限制 如果启用,将定期检查限制连接,如果空闲时间超过minEvictableIdleTimeMillis 则释放连接 ( 参考testWhileIdle ) +maxIdle = 8 +# (int) 最小空闲连接: 连接池中容许保持空闲状态的最小连接数量, 低于这个数量将创建新的连接, 如果设置为0 则不创建 如果连接验证失败将缩小这个值( 参考testWhileIdle ) +minIdle = 0 +# (int) 最大等待时间: 当没有可用连接时, 连接池等待连接被归还的最大时间( 以毫秒计数), 超过时间则抛出异常, 如果设置为-1 表示无限等待 +maxWait = 30000 +# (String) SQL 查询, 用来验证从连接池取出的连接, 在将连接返回给调用者之前。 如果指定, 则查询必须是一个SQL SELECT 并且必须返回至少一行记录 查询不必返回记录,但这样将不能抛出SQL异常 +validationQuery = SELECT 1 +# (boolean) 指明是否在从池中取出连接前进行检验, 如果检验失败, 则从池中去除连接并尝试取出另一个。注意: 设置为true 后如果要生效,validationQuery 参数必须设置为非空字符串 参考validationInterval以获得更有效的验证 +testOnBorrow = false +# (boolean) 指明是否在归还到池中前进行检验 注意: 设置为true 后如果要生效,validationQuery 参数必须设置为非空字符串 +testOnReturn = false +# (boolean) 指明连接是否被空闲连接回收器( 如果有) 进行检验。 如果检测失败, 则连接将被从池中去除。注意: 设置为true 后如果要生效,validationQuery 参数必须设置为非空字符串 +testWhileIdle = false \ No newline at end of file diff --git a/hutool-db/src/test/resources/config/example/db-example-druid.setting b/hutool-db/src/test/resources/config/example/db-example-druid.setting new file mode 100644 index 000000000..c07b33ba7 --- /dev/null +++ b/hutool-db/src/test/resources/config/example/db-example-druid.setting @@ -0,0 +1,55 @@ +#=================================================================== +# 数据库配置文件样例 +# DsFactory默认读取的配置文件是config/db.setting +# db.setting的配置包括两部分:基本连接信息和连接池配置信息。 +# 基本连接信息所有连接池都支持,连接池配置信息根据不同的连接池,连接池配置是根据连接池相应的配置项移植而来 +#=================================================================== + +#---------------------------------------------------------------------------------------------------------------- +## 基本配置信息 +# JDBC URL,根据不同的数据库,使用相应的JDBC连接字符串 +url = jdbc:mysql://:/ +# 用户名,此处也可以使用 user 代替 +username = 用户名 +# 密码,此处也可以使用 pass 代替 +password = 密码 +# JDBC驱动名,可选(Hutool会自动识别) +driver = com.mysql.jdbc.Driver +# 是否在日志中显示执行的SQL +showSql = true +# 是否格式化显示的SQL +formatSql = true + + +#---------------------------------------------------------------------------------------------------------------- +## 连接池配置项 + +## ---------------------------------------------------- Druid +# 初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时 +initialSize = 0 +# 最大连接池数量 +maxActive = 8 +# 最小连接池数量 +minIdle = 0 +# 获取连接时最大等待时间,单位毫秒。配置了maxWait之后, 缺省启用公平锁,并发效率会有所下降, 如果需要可以通过配置useUnfairLock属性为true使用非公平锁。 +maxWait = 0 +# 是否缓存preparedStatement,也就是PSCache。 PSCache对支持游标的数据库性能提升巨大,比如说oracle。 在mysql5.5以下的版本中没有PSCache功能,建议关闭掉。作者在5.5版本中使用PSCache,通过监控界面发现PSCache有缓存命中率记录, 该应该是支持PSCache。 +poolPreparedStatements = false +# 要启用PSCache,必须配置大于0,当大于0时, poolPreparedStatements自动触发修改为true。 在Druid中,不会存在Oracle下PSCache占用内存过多的问题, 可以把这个数值配置大一些,比如说100 +maxOpenPreparedStatements = -1 +# 用来检测连接是否有效的sql,要求是一个查询语句。 如果validationQuery为null,testOnBorrow、testOnReturn、 testWhileIdle都不会其作用。 +validationQuery = SELECT 1 +# 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 +testOnBorrow = true +# 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能 +testOnReturn = false +# 建议配置为true,不影响性能,并且保证安全性。 申请连接的时候检测,如果空闲时间大于 timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 +testWhileIdle = false +# 有两个含义: 1) Destroy线程会检测连接的间隔时间 2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明 +timeBetweenEvictionRunsMillis = 60000 +# 物理连接初始化的时候执行的sql +connectionInitSqls = SELECT 1 +# 属性类型是字符串,通过别名的方式配置扩展插件, 常用的插件有: 监控统计用的filter:stat 日志用的filter:log4j 防御sql注入的filter:wall +filters = stat +# 类型是List, 如果同时配置了filters和proxyFilters, 是组合关系,并非替换关系 +proxyFilters = \ No newline at end of file diff --git a/hutool-db/src/test/resources/config/example/db-example-hikari.setting b/hutool-db/src/test/resources/config/example/db-example-hikari.setting new file mode 100644 index 000000000..e41a7dbca --- /dev/null +++ b/hutool-db/src/test/resources/config/example/db-example-hikari.setting @@ -0,0 +1,43 @@ +#=================================================================== +# 数据库配置文件样例 +# DsFactory默认读取的配置文件是config/db.setting +# db.setting的配置包括两部分:基本连接信息和连接池配置信息。 +# 基本连接信息所有连接池都支持,连接池配置信息根据不同的连接池,连接池配置是根据连接池相应的配置项移植而来 +#=================================================================== + +#---------------------------------------------------------------------------------------------------------------- +## 基本配置信息 +# JDBC URL,根据不同的数据库,使用相应的JDBC连接字符串 +url = jdbc:mysql://:/ +# 用户名,此处也可以使用 user 代替 +username = 用户名 +# 密码,此处也可以使用 pass 代替 +password = 密码 +# JDBC驱动名,可选(Hutool会自动识别) +driver = com.mysql.jdbc.Driver +# 是否在日志中显示执行的SQL +showSql = true +# 是否格式化显示的SQL +formatSql = true + + +#---------------------------------------------------------------------------------------------------------------- +## 连接池配置项 + +## ---------------------------------------------------- HikariCP +# 自动提交 +autoCommit = true +# 等待连接池分配连接的最大时长(毫秒),超过这个时长还没可用的连接则发生SQLException, 缺省:30秒 +connectionTimeout = 30000 +# 一个连接idle状态的最大时长(毫秒),超时则被释放(retired),缺省:10分钟 +idleTimeout = 600000 +# 一个连接的生命时长(毫秒),超时而且没被使用则被释放(retired),缺省:30分钟,建议设置比数据库超时时长少30秒,参考MySQL wait_timeout参数(show variables like '%timeout%';) +maxLifetime = 1800000 +# 获取连接前的测试SQL +connectionTestQuery = SELECT 1 +# 最小闲置连接数 +minimumIdle = 10 +# 连接池中允许的最大连接数。缺省值:10;推荐的公式:((core_count * 2) + effective_spindle_count) +maximumPoolSize = 10 +# 连接只读数据库时配置为true, 保证安全 +readOnly = false \ No newline at end of file diff --git a/hutool-db/src/test/resources/config/example/db-example-tomcat.setting b/hutool-db/src/test/resources/config/example/db-example-tomcat.setting new file mode 100644 index 000000000..f0bc40d14 --- /dev/null +++ b/hutool-db/src/test/resources/config/example/db-example-tomcat.setting @@ -0,0 +1,51 @@ +#=================================================================== +# 数据库配置文件样例 +# DsFactory默认读取的配置文件是config/db.setting +# db.setting的配置包括两部分:基本连接信息和连接池配置信息。 +# 基本连接信息所有连接池都支持,连接池配置信息根据不同的连接池,连接池配置是根据连接池相应的配置项移植而来 +#=================================================================== + +#---------------------------------------------------------------------------------------------------------------- +## 基本配置信息 +# JDBC URL,根据不同的数据库,使用相应的JDBC连接字符串 +url = jdbc:mysql://:/ +# 用户名,此处也可以使用 user 代替 +username = 用户名 +# 密码,此处也可以使用 pass 代替 +password = 密码 +# JDBC驱动名,可选(Hutool会自动识别) +driver = com.mysql.jdbc.Driver +# 是否在日志中显示执行的SQL +showSql = true +# 是否格式化显示的SQL +formatSql = true + + +#---------------------------------------------------------------------------------------------------------------- +## 连接池配置项 + +## ---------------------------------------------------- Tomcat-Jdbc-Pool +# (boolean) 连接池创建的连接的默认的auto-commit 状态 +defaultAutoCommit = true +# (boolean) 连接池创建的连接的默认的read-only 状态。 如果没有设置则setReadOnly 方法将不会被调用。 ( 某些驱动不支持只读模式, 比如:Informix) +defaultReadOnly = false +# (String) 连接池创建的连接的默认的TransactionIsolation 状态。 下面列表当中的某一个: ( 参考javadoc) NONE READ_COMMITTED EAD_UNCOMMITTED REPEATABLE_READ SERIALIZABLE +defaultTransactionIsolation = NONE +# (int) 初始化连接: 连接池启动时创建的初始化连接数量,1。2 版本后支持 +initialSize = 10 +# (int) 最大活动连接: 连接池在同一时间能够分配的最大活动连接的数量, 如果设置为非正数则表示不限制 +maxActive = 100 +# (int) 最大空闲连接: 连接池中容许保持空闲状态的最大连接数量, 超过的空闲连接将被释放, 如果设置为负数表示不限制 如果启用,将定期检查限制连接,如果空闲时间超过minEvictableIdleTimeMillis 则释放连接 ( 参考testWhileIdle ) +maxIdle = 8 +# (int) 最小空闲连接: 连接池中容许保持空闲状态的最小连接数量, 低于这个数量将创建新的连接, 如果设置为0 则不创建 如果连接验证失败将缩小这个值( 参考testWhileIdle ) +minIdle = 0 +# (int) 最大等待时间: 当没有可用连接时, 连接池等待连接被归还的最大时间( 以毫秒计数), 超过时间则抛出异常, 如果设置为-1 表示无限等待 +maxWait = 30000 +# (String) SQL 查询, 用来验证从连接池取出的连接, 在将连接返回给调用者之前。 如果指定, 则查询必须是一个SQL SELECT 并且必须返回至少一行记录 查询不必返回记录,但这样将不能抛出SQL异常 +validationQuery = SELECT 1 +# (boolean) 指明是否在从池中取出连接前进行检验, 如果检验失败, 则从池中去除连接并尝试取出另一个。注意: 设置为true 后如果要生效,validationQuery 参数必须设置为非空字符串 参考validationInterval以获得更有效的验证 +testOnBorrow = false +# (boolean) 指明是否在归还到池中前进行检验 注意: 设置为true 后如果要生效,validationQuery 参数必须设置为非空字符串 +testOnReturn = false +# (boolean) 指明连接是否被空闲连接回收器( 如果有) 进行检验。 如果检测失败, 则连接将被从池中去除。注意: 设置为true 后如果要生效,validationQuery 参数必须设置为非空字符串 +testWhileIdle = false \ No newline at end of file diff --git a/hutool-db/src/test/resources/config/example/mongo-example.setting b/hutool-db/src/test/resources/config/example/mongo-example.setting new file mode 100644 index 000000000..c10c15fa4 --- /dev/null +++ b/hutool-db/src/test/resources/config/example/mongo-example.setting @@ -0,0 +1,29 @@ +#-------------------------------------- +# MongoDB 连接设定 +# author xiaoleilu +#-------------------------------------- + +#每个主机答应的连接数(每个主机的连接池大小),当连接池被用光时,会被阻塞住 ,默以为10 --int +connectionsPerHost=100 +#线程队列数,它以connectionsPerHost值相乘的结果就是线程队列最大值。如果连接线程排满了队列就会抛出“Out of semaphores to get db”错误 --int +threadsAllowedToBlockForConnectionMultiplier=10 +#被阻塞线程从连接池获取连接的最长等待时间(ms) --int +maxWaitTime = 120000 +#在建立(打开)套接字连接时的超时时间(ms),默以为0(无穷) --int +connectTimeout=0 +#套接字超时时间;该值会被传递给Socket.setSoTimeout(int)。默以为0(无穷) --int +socketTimeout=0 +#是否打开长连接. defaults to false --boolean +socketKeepAlive=false + +user = test1 +pass = 123456 +database = test + +#---------------------------------- MongoDB实例连接 +[master] +host = 127.0.0.1:27017 + +[slave] +host = 127.0.0.1:27017 +#----------------------------------------------------- \ No newline at end of file diff --git a/hutool-db/src/test/resources/config/redis.setting b/hutool-db/src/test/resources/config/redis.setting new file mode 100644 index 000000000..085a97010 --- /dev/null +++ b/hutool-db/src/test/resources/config/redis.setting @@ -0,0 +1,59 @@ +#---------------------------------------------------------------------------------- +# Redis客户端配置样例 +# 每一个分组代表一个Redis实例 +# 无分组的Pool配置为所有分组的共用配置,如果分组自己定义Pool配置,则覆盖共用配置 +# 池配置来自于:https://www.cnblogs.com/jklk/p/7095067.html +#---------------------------------------------------------------------------------- + +#----- 默认(公有)配置 +# 地址,默认localhost +host = localhost +# 端口,默认6379 +port = 6379 +# 超时,默认2000 +timeout = 2000 +# 连接超时,默认timeout +connectionTimeout = 2000 +# 读取超时,默认timeout +soTimeout = 2000 +# 密码,默认无 +password = +# 数据库序号,默认0 +database = 0 +# 客户端名,默认"Hutool" +clientName = Hutool +# SSL连接,默认false +ssl = false; + +#----- 自定义分组的连接 +[custom] +# 地址,默认localhost +host = localhost +# 连接耗尽时是否阻塞, false报异常,ture阻塞直到超时, 默认true +BlockWhenExhausted = true; +# 设置的逐出策略类名, 默认DefaultEvictionPolicy(当连接超过最大空闲时间,或连接数超过最大空闲连接数) +evictionPolicyClassName = org.apache.commons.pool2.impl.DefaultEvictionPolicy +# 是否启用pool的jmx管理功能, 默认true +jmxEnabled = true; +# 是否启用后进先出, 默认true +lifo = true; +# 最大空闲连接数, 默认8个 +maxIdle = 8 +# 最小空闲连接数, 默认0 +minIdle = 0 +# 最大连接数, 默认8个 +maxTotal = 8 +# 获取连接时的最大等待毫秒数(如果设置为阻塞时BlockWhenExhausted),如果超时就抛异常, 小于零:阻塞不确定的时间, 默认-1 +maxWaitMillis = -1 +# 逐出连接的最小空闲时间 默认1800000毫秒(30分钟) +minEvictableIdleTimeMillis = 1800000 +# 每次逐出检查时 逐出的最大数目 如果为负数就是 : 1/abs(n), 默认3 +numTestsPerEvictionRun = 3; +# 对象空闲多久后逐出, 当空闲时间>该值 且 空闲连接>最大空闲数 时直接逐出,不再根据MinEvictableIdleTimeMillis判断 (默认逐出策略) +SoftMinEvictableIdleTimeMillis = 1800000 +# 在获取连接的时候检查有效性, 默认false +testOnBorrow = false +# 在空闲时检查有效性, 默认false +testWhileIdle = false +# 逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1 +timeBetweenEvictionRunsMillis = -1 diff --git a/hutool-db/src/test/resources/logback.xml b/hutool-db/src/test/resources/logback.xml new file mode 100644 index 000000000..3a29bddf3 --- /dev/null +++ b/hutool-db/src/test/resources/logback.xml @@ -0,0 +1,17 @@ + + + + + + + + + ${format} + + + + + + + + \ No newline at end of file diff --git a/hutool-db/src/test/resources/simplelogger.properties b/hutool-db/src/test/resources/simplelogger.properties new file mode 100644 index 000000000..3c1930e35 --- /dev/null +++ b/hutool-db/src/test/resources/simplelogger.properties @@ -0,0 +1,2 @@ +org.slf4j.simpleLogger.defaultLogLevel = debug +org.slf4j.simpleLogger.log.com.zaxxer.hikari = info \ No newline at end of file diff --git a/hutool-db/test.db b/hutool-db/test.db new file mode 100644 index 0000000000000000000000000000000000000000..d9e002433c848d9d7fcc5515a616aba1494572ff GIT binary patch literal 24576 zcmeI)F>ljA7=YoklNuV?94ZQh0qGQpkgBCLW$H#i7K(6_5*&#vAh$6kQXAC78JJMn z0Er3q&Sb9;;s+ox@E=eSGfQXgXoDa_mktf#we`im^L^*LNA?iO_Px77D~Z*kC!NhG zQP;&;VOipaQbLHn4oAm?j+Bm!jN;}$&byDcx4^1&zH>JZa zOvP%elRR!lPycAM9=Dt3jDu!gW=1}YSMKQO>)*HX<(N&C=^%gr0tg_000IagfB*srATY`T=db85?f2TPB#gUBUB3XZ zSUu-D;_^^!kezhIMeEY!w~sq}?_apZrKN?1VsX*4O|=l;U%%S>@Mg-h4vL4ZegcrM BfhPa} literal 0 HcmV?d00001 diff --git a/hutool-dfa/pom.xml b/hutool-dfa/pom.xml new file mode 100644 index 000000000..0c40255d4 --- /dev/null +++ b/hutool-dfa/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + jar + + + cn.hutool + hutool-parent + 4.6.2-SNAPSHOT + + + hutool-dfa + ${project.artifactId} + Hutool 基于DFA的关键词查找 + + + + cn.hutool + hutool-core + ${project.parent.version} + + + cn.hutool + hutool-json + ${project.parent.version} + + + diff --git a/hutool-dfa/src/main/java/cn/hutool/dfa/SensitiveUtil.java b/hutool-dfa/src/main/java/cn/hutool/dfa/SensitiveUtil.java new file mode 100644 index 000000000..16e49c8a9 --- /dev/null +++ b/hutool-dfa/src/main/java/cn/hutool/dfa/SensitiveUtil.java @@ -0,0 +1,161 @@ +package cn.hutool.dfa; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; + +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; + +/** + * 敏感词工具类 + * @author Looly + * + */ +public final class SensitiveUtil { +// private static final Log log = LogFactory.get(); + + public static final char DEFAULT_SEPARATOR = StrUtil.C_COMMA; + private static WordTree sensitiveTree = new WordTree(); + + /** + * @return 是否已经被初始化 + */ + public static boolean isInited(){ + return !sensitiveTree.isEmpty(); + } + + /** + * 初始化敏感词树 + * @param isAsync 是否异步初始化 + * @param sensitiveWords 敏感词列表 + */ + public static void init(final Collection sensitiveWords, boolean isAsync){ + if(isAsync){ + ThreadUtil.execAsync(new Callable(){ + @Override + public Boolean call() throws Exception { + init(sensitiveWords); + return true; + } + + }); + }else{ + init(sensitiveWords); + } + } + + /** + * 初始化敏感词树 + * @param sensitiveWords 敏感词列表 + */ + public static void init(Collection sensitiveWords){ + sensitiveTree.clear(); + sensitiveTree.addWords(sensitiveWords); +// log.debug("Sensitive init finished, sensitives: {}", sensitiveWords); + } + + /** + * 初始化敏感词树 + * @param sensitiveWords 敏感词列表组成的字符串 + * @param isAsync 是否异步初始化 + * @param separator 分隔符 + */ + public static void init(String sensitiveWords, char separator, boolean isAsync){ + if(StrUtil.isNotBlank(sensitiveWords)){ + init(StrUtil.split(sensitiveWords, separator), isAsync); + } + } + + /** + * 初始化敏感词树,使用逗号分隔每个单词 + * @param sensitiveWords 敏感词列表组成的字符串 + * @param isAsync 是否异步初始化 + */ + public static void init(String sensitiveWords, boolean isAsync){ + init(sensitiveWords, DEFAULT_SEPARATOR, isAsync); + } + + /** + * 是否包含敏感词 + * @param text 文本 + * @return 是否包含 + */ + public static boolean containsSensitive(String text){ + return sensitiveTree.isMatch(text); + } + + /** + * 是否包含敏感词 + * @param obj bean,会被转为JSON字符串 + * @return 是否包含 + */ + public static boolean containsSensitive(Object obj){ + return sensitiveTree.isMatch(JSONUtil.toJsonStr(obj)); + } + + /** + * 查找敏感词,返回找到的第一个敏感词 + * @param text 文本 + * @return 敏感词 + */ + public static String getFindedFirstSensitive(String text){ + return sensitiveTree.match(text); + } + + /** + * 查找敏感词,返回找到的第一个敏感词 + * @param obj bean,会被转为JSON字符串 + * @return 敏感词 + */ + public static String getFindedFirstSensitive(Object obj){ + return sensitiveTree.match(JSONUtil.toJsonStr(obj)); + } + + /** + * 查找敏感词,返回找到的所有敏感词 + * @param text 文本 + * @return 敏感词 + */ + public static List getFindedAllSensitive(String text){ + return sensitiveTree.matchAll(text); + } + + /** + * 查找敏感词,返回找到的所有敏感词
+ * 密集匹配原则:假如关键词有 ab,b,文本是abab,将匹配 [ab,b,ab]
+ * 贪婪匹配(最长匹配)原则:假如关键字a,ab,最长匹配将匹配[a, ab] + * + * @param text 文本 + * @param isDensityMatch 是否使用密集匹配原则 + * @param isGreedMatch 是否使用贪婪匹配(最长匹配)原则 + * @return 敏感词 + */ + public static List getFindedAllSensitive(String text, boolean isDensityMatch, boolean isGreedMatch){ + return sensitiveTree.matchAll(text, -1, isDensityMatch, isGreedMatch); + } + + /** + * 查找敏感词,返回找到的所有敏感词 + * @param bean 对象,会被转为JSON + * @return 敏感词 + */ + public static List getFindedAllSensitive(Object bean){ + return sensitiveTree.matchAll(JSONUtil.toJsonStr(bean)); + } + + /** + * 查找敏感词,返回找到的所有敏感词
+ * 密集匹配原则:假如关键词有 ab,b,文本是abab,将匹配 [ab,b,ab]
+ * 贪婪匹配(最长匹配)原则:假如关键字a,ab,最长匹配将匹配[a, ab] + * + * @param bean 对象,会被转为JSON + * @param isDensityMatch 是否使用密集匹配原则 + * @param isGreedMatch 是否使用贪婪匹配(最长匹配)原则 + * @return 敏感词 + */ + public static List getFindedAllSensitive(Object bean, boolean isDensityMatch, boolean isGreedMatch){ + return getFindedAllSensitive(JSONUtil.toJsonStr(bean), isDensityMatch, isGreedMatch); + } +} diff --git a/hutool-dfa/src/main/java/cn/hutool/dfa/StopChar.java b/hutool-dfa/src/main/java/cn/hutool/dfa/StopChar.java new file mode 100644 index 000000000..432bcb23f --- /dev/null +++ b/hutool-dfa/src/main/java/cn/hutool/dfa/StopChar.java @@ -0,0 +1,49 @@ +package cn.hutool.dfa; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * 过滤词及一些简单处理 + * + * @author Looly + */ +public class StopChar { + /** 不需要处理的词,如标点符号、空格等 */ + public static final Set STOP_WORD = new HashSet<>(Arrays.asList(new Character[] { ' ', '\'', '、', '。', + '·', 'ˉ', 'ˇ', '々', '—', '~', '‖', '…', '‘', '’', '“', '”', '〔', '〕', '〈', '〉', '《', '》', '「', '」', '『', + '』', '〖', '〗', '【', '】', '±', '+', '-', '×', '÷', '∧', '∨', '∑', '∏', '∪', '∩', '∈', '√', '⊥', '⊙', '∫', + '∮', '≡', '≌', '≈', '∽', '∝', '≠', '≮', '≯', '≤', '≥', '∞', '∶', '∵', '∴', '∷', '♂', '♀', '°', '′', '〃', + '℃', '$', '¤', '¢', '£', '‰', '§', '☆', '★', '〇', '○', '●', '◎', '◇', '◆', '□', '■', '△', '▽', '⊿', '▲', + '▼', '◣', '◤', '◢', '◥', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█', '▉', '▊', '▋', '▌', '▍', '▎', '▏', '▓', + '※', '→', '←', '↑', '↓', '↖', '↗', '↘', '↙', '〓', 'ⅰ', 'ⅱ', 'ⅲ', 'ⅳ', 'ⅴ', 'ⅵ', 'ⅶ', 'ⅷ', 'ⅸ', 'ⅹ', '①', + '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩', '⒈', '⒉', '⒊', '⒋', '⒌', '⒍', '⒎', '⒏', '⒐', '⒑', '⒒', '⒓', + '⒔', '⒕', '⒖', '⒗', '⒘', '⒙', '⒚', '⒛', '⑴', '⑵', '⑶', '⑷', '⑸', '⑹', '⑺', '⑻', '⑼', '⑽', '⑾', '⑿', '⒀', + '⒁', '⒂', '⒃', '⒄', '⒅', '⒆', '⒇', 'Ⅰ', 'Ⅱ', 'Ⅲ', 'Ⅳ', 'Ⅴ', 'Ⅵ', 'Ⅶ', 'Ⅷ', 'Ⅸ', 'Ⅹ', 'Ⅺ', 'Ⅻ', '!', '”', + '#', '¥', '%', '&', '’', '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', ':', ';', '<', '=', '>', '?', '@', '〔', '\', '〕', '^', '_', '‘', '{', '|', '}', '∏', 'Ρ', '∑', + 'Υ', 'Φ', 'Χ', 'Ψ', 'Ω', 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ', 'ι', 'κ', 'λ', 'μ', 'ν', 'ξ', 'ο', 'π', + 'ρ', 'σ', 'τ', 'υ', 'φ', 'χ', 'ψ', 'ω', '(', ')', '〔', '〕', '^', '﹊', '﹍', '╭', '╮', '╰', '╯', '', '_', + '', '^', '(', '^', ':', '!', '/', '\\', '\"', '<', '>', '`', '·', '。', '{', '}', '~', '~', '(', ')', '-', + '√', '$', '@', '*', '&', '#', '卐', '㎎', '㎏', '㎜', '㎝', '㎞', '㎡', '㏄', '㏎', '㏑', '㏒', '㏕' })); + + /** + * 判断指定的词是否是不处理的词。 + * 如果参数为空,则返回true,因为空也属于不处理的字符。 + * + * @param ch 指定的词 + * @return 是否是不处理的词 + */ + public static boolean isStopChar(char ch) { + return Character.isWhitespace(ch) || STOP_WORD.contains(ch); + } + + /** + * 是否为合法字符(待处理字符) + * @param ch 指定的词 + * @return 是否为合法字符(待处理字符) + */ + public static boolean isNotStopChar(char ch) { + return false == isStopChar(ch); + } +} diff --git a/hutool-dfa/src/main/java/cn/hutool/dfa/WordTree.java b/hutool-dfa/src/main/java/cn/hutool/dfa/WordTree.java new file mode 100644 index 000000000..49b3cdd76 --- /dev/null +++ b/hutool-dfa/src/main/java/cn/hutool/dfa/WordTree.java @@ -0,0 +1,233 @@ +package cn.hutool.dfa; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; + +/** + * DFA(Deterministic Finite Automaton 确定有穷自动机) + * DFA单词树(以下简称单词树),常用于在某大段文字中快速查找某几个关键词是否存在。
+ * 单词树使用group区分不同的关键字集合,不同的分组可以共享树枝,避免重复建树。
+ * 单词树使用树状结构表示一组单词。
+ * 例如:红领巾,红河构建树后为:
+ * 红
+ * / \
+ * 领 河
+ * /
+ * 巾
+ *其中每个节点都是一个WordTree对象,查找时从上向下查找。
+ * @author Looly + * + */ +public class WordTree extends HashMap{ + private static final long serialVersionUID = -4646423269465809276L; + + /** + * 敏感词字符末尾标识,用于标识单词末尾字符 + */ + private Set endCharacterSet = new HashSet<>(); + + //--------------------------------------------------------------------------------------- Constructor start + /** + * 默认构造 + */ + public WordTree() { + } + //--------------------------------------------------------------------------------------- Constructor start + + //------------------------------------------------------------------------------- add word + + /** + * 增加一组单词 + * @param words 单词集合 + */ + public void addWords(Collection words){ + if(false == (words instanceof Set)){ + words = new HashSet<>(words); + } + for (String word : words) { + addWord(word); + } + } + + /** + * 增加一组单词 + * @param words 单词数组 + */ + public void addWords(String... words){ + HashSet wordsSet = CollectionUtil.newHashSet(words); + for (String word : wordsSet) { + addWord(word); + } + } + + /** + * 添加单词,使用默认类型 + * @param word 单词 + */ + public void addWord(String word) { + WordTree parent = null; + WordTree current = this; + WordTree child; + char currentChar = 0; + int length = word.length(); + for(int i = 0; i < length; i++){ + currentChar = word.charAt(i); + if(false == StopChar.isStopChar(currentChar)){//只处理合法字符 + child = current.get(currentChar); + if(child == null){ + //无子类,新建一个子节点后存放下一个字符 + child = new WordTree(); + current.put(currentChar, child); + } + parent = current; + current = child; + } + } + if(null != parent){ + parent.setEnd(currentChar); + } + } + + //------------------------------------------------------------------------------- match + /** + * 指定文本是否包含树中的词 + * @param text 被检查的文本 + * @return 是否包含 + */ + public boolean isMatch(String text){ + if(null == text){ + return false; + } + return null != match(text); + } + + /** + * 获得第一个匹配的关键字 + * @param text 被检查的文本 + * @return 匹配到的关键字 + */ + public String match(String text){ + if(null == text){ + return null; + } + List matchAll = matchAll(text, 1); + if(CollectionUtil.isNotEmpty(matchAll)){ + return matchAll.get(0); + } + return null; + } + + //------------------------------------------------------------------------------- match all + /** + * 找出所有匹配的关键字 + * @param text 被检查的文本 + * @return 匹配的词列表 + */ + public List matchAll(String text) { + return matchAll(text, -1); + } + + /** + * 找出所有匹配的关键字 + * @param text 被检查的文本 + * @param limit 限制匹配个数 + * @return 匹配的词列表 + */ + public List matchAll(String text, int limit) { + return matchAll(text, limit, false, false); + } + + /** + * 找出所有匹配的关键字
+ * 密集匹配原则:假如关键词有 ab,b,文本是abab,将匹配 [ab,b,ab]
+ * 贪婪匹配(最长匹配)原则:假如关键字a,ab,最长匹配将匹配[a, ab] + * + * @param text 被检查的文本 + * @param limit 限制匹配个数 + * @param isDensityMatch 是否使用密集匹配原则 + * @param isGreedMatch 是否使用贪婪匹配(最长匹配)原则 + * @return 匹配的词列表 + */ + public List matchAll(String text, int limit, boolean isDensityMatch, boolean isGreedMatch) { + if(null == text){ + return null; + } + + List findedWords = new ArrayList(); + WordTree current = this; + int length = text.length(); + StringBuilder wordBuffer;//存放查找到的字符缓存。完整出现一个词时加到findedWords中,否则清空 + char currentChar; + for (int i = 0; i < length; i++) { + wordBuffer = StrUtil.builder(); + for (int j = i; j < length; j++) { + currentChar = text.charAt(j); +// Console.log("i: {}, j: {}, currentChar: {}", i, j, currentChar); + if(StopChar.isStopChar(currentChar)){ + if(wordBuffer.length() > 0){ + //做为关键词中间的停顿词被当作关键词的一部分被返回 + wordBuffer.append(currentChar); + }else{ + //停顿词做为关键词的第一个字符时需要跳过 + i++; + } + continue; + }else if(false == current.containsKey(currentChar)){ + //非关键字符被整体略过,重新以下个字符开始检查 + break; + } + wordBuffer.append(currentChar); + if(current.isEnd(currentChar)){ + //到达单词末尾,关键词成立,从此词的下一个位置开始查找 + findedWords.add(wordBuffer.toString()); + if(limit > 0 && findedWords.size() >= limit){ + //超过匹配限制个数,直接返回 + return findedWords; + } + if(false == isDensityMatch){ + //如果非密度匹配,跳过匹配到的词 + i = j; + } + if(false == isGreedMatch){ + //如果懒惰匹配(非贪婪匹配)。当遇到第一个结尾标记就结束本轮匹配 + break; + } + } + current = current.get(currentChar); + if(null == current){ + break; + } + } + current = this; + } + return findedWords; + } + + + //--------------------------------------------------------------------------------------- Private method start + /** + * 是否末尾 + * @param c 检查的字符 + * @return 是否末尾 + */ + private boolean isEnd(Character c){ + return this.endCharacterSet.contains(c); + } + + /** + * 设置是否到达末尾 + * @param c 设置结尾的字符 + */ + private void setEnd(Character c){ + if(null != c){ + this.endCharacterSet.add(c); + } + } + //--------------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-dfa/src/main/java/cn/hutool/dfa/package-info.java b/hutool-dfa/src/main/java/cn/hutool/dfa/package-info.java new file mode 100644 index 000000000..1e0dd7135 --- /dev/null +++ b/hutool-dfa/src/main/java/cn/hutool/dfa/package-info.java @@ -0,0 +1,9 @@ +/** + * DFA全称为:Deterministic Finite Automaton,即确定有穷自动机。
+ * 解释起来原理其实也不难,就是用所有关键字构造一棵树,然后用正文遍历这棵树,遍历到叶子节点即表示文章中存在这个关键字。
+ * 我们暂且忽略构建关键词树的时间,每次查找正文只需要O(n)复杂度就可以搞定。
+ * + * @author looly + * + */ +package cn.hutool.dfa; \ No newline at end of file diff --git a/hutool-dfa/src/test/java/cn/hutool/dfa/test/DfaTest.java b/hutool-dfa/src/test/java/cn/hutool/dfa/test/DfaTest.java new file mode 100644 index 000000000..cd4433d55 --- /dev/null +++ b/hutool-dfa/src/test/java/cn/hutool/dfa/test/DfaTest.java @@ -0,0 +1,113 @@ +package cn.hutool.dfa.test; + +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.dfa.WordTree; + +/** + * DFA单元测试 + * + * @author Looly + * + */ +public class DfaTest { + + // 构建被查询的文本 + String text = "我有一颗大土豆,刚出锅的"; + + @Test + public void matchAllTest() { + // 构建查询树 + WordTree tree = buildWordTree(); + + // ----------------------------------------------------------------------------------------------------------------------------------- + // 情况一:标准匹配,匹配到最短关键词,并跳过已经匹配的关键词 + // 匹配到【大】,就不再继续匹配了,因此【大土豆】不匹配 + // 匹配到【刚出锅】,就跳过这三个字了,因此【出锅】不匹配(由于刚首先被匹配,因此长的被匹配,最短匹配只针对第一个字相同选最短) + List matchAll = tree.matchAll(text, -1, false, false); + Assert.assertEquals(matchAll, CollectionUtil.newArrayList("大", "土豆", "刚出锅")); + } + + /** + * 密集匹配原则(最短匹配)测试 + */ + @Test + public void densityMatchTest() { + // 构建查询树 + WordTree tree = buildWordTree(); + + // ----------------------------------------------------------------------------------------------------------------------------------- + // 情况二:匹配到最短关键词,不跳过已经匹配的关键词 + // 【大】被匹配,最短匹配原则【大土豆】被跳过,【土豆继续被匹配】 + // 【刚出锅】被匹配,由于不跳过已经匹配的词,【出锅】被匹配 + List matchAll = tree.matchAll(text, -1, true, false); + Assert.assertEquals(matchAll, CollectionUtil.newArrayList("大", "土豆", "刚出锅", "出锅")); + } + + /** + * 贪婪匹配原则测试 + */ + @Test + public void greedMatchTest() { + // 构建查询树 + WordTree tree = buildWordTree(); + + // ----------------------------------------------------------------------------------------------------------------------------------- + // 情况三:匹配到最长关键词,跳过已经匹配的关键词 + // 匹配到【大】,由于到最长匹配,因此【大土豆】接着被匹配 + // 由于【大土豆】被匹配,【土豆】被跳过,由于【刚出锅】被匹配,【出锅】被跳过 + List matchAll = tree.matchAll(text, -1, false, true); + Assert.assertEquals(matchAll, CollectionUtil.newArrayList("大", "大土豆", "刚出锅")); + + } + + /** + * 密集匹配原则(最短匹配)和贪婪匹配原则测试 + */ + @Test + public void densityAndGreedMatchTest() { + // 构建查询树 + WordTree tree = buildWordTree(); + + // ----------------------------------------------------------------------------------------------------------------------------------- + // 情况四:匹配到最长关键词,不跳过已经匹配的关键词(最全关键词) + // 匹配到【大】,由于到最长匹配,因此【大土豆】接着被匹配,由于不跳过已经匹配的关键词,土豆继续被匹配 + // 【刚出锅】被匹配,由于不跳过已经匹配的词,【出锅】被匹配 + List matchAll = tree.matchAll(text, -1, true, true); + Assert.assertEquals(matchAll, CollectionUtil.newArrayList("大", "大土豆", "土豆", "刚出锅", "出锅")); + + } + + /** + * 停顿词测试 + */ + @Test + public void stopWordTest() { + WordTree tree = new WordTree(); + tree.addWord("tio"); + + List all = tree.matchAll("AAAAAAAt-ioBBBBBBB"); + Assert.assertEquals(all, CollectionUtil.newArrayList("t-io")); + } + + // ---------------------------------------------------------------------------------------------------------- + /** + * 构建查找树 + * + * @return 查找树 + */ + private WordTree buildWordTree() { + // 构建查询树 + WordTree tree = new WordTree(); + tree.addWord("大"); + tree.addWord("大土豆"); + tree.addWord("土豆"); + tree.addWord("刚出锅"); + tree.addWord("出锅"); + return tree; + } +} diff --git a/hutool-extra/pom.xml b/hutool-extra/pom.xml new file mode 100644 index 000000000..bb3ba48b1 --- /dev/null +++ b/hutool-extra/pom.xml @@ -0,0 +1,204 @@ + + + 4.0.0 + + jar + + + cn.hutool + hutool-parent + 4.6.2-SNAPSHOT + + + hutool-extra + ${project.artifactId} + Hutool 扩展工具类(提供其它类库的封装) + + + + 2.0 + 2.9.8 + 1.3.0 + 2.3.28 + 3.4 + 3.0.11.RELEASE + 1.6.2 + 0.1.55 + 3.3.3 + 3.6 + 4.0.0 + 3.1.0 + + + + + cn.hutool + hutool-core + ${project.parent.version} + + + cn.hutool + hutool-setting + ${project.parent.version} + + + javax.servlet + javax.servlet-api + ${servlet-api.version} + provided + true + + + + + org.apache.velocity + velocity-engine-core + ${velocity.version} + compile + true + + + com.ibeetl + beetl + ${beetl.version} + compile + true + + + org.rythmengine + rythm-engine + ${rythm.version} + compile + true + + + org.freemarker + freemarker + ${freemarker.version} + compile + true + + + com.jfinal + enjoy + ${enjoy.version} + compile + true + + + org.thymeleaf + thymeleaf + ${thymeleaf.version} + compile + true + + + + + com.sun.mail + javax.mail + ${mail.version} + compile + true + + + + + com.jcraft + jsch + ${jsch.version} + compile + true + + + + com.google.zxing + core + ${zxing.version} + compile + true + + + + commons-net + commons-net + ${net.version} + compile + true + + + + com.vdurmont + emoji-java + ${emoji-java.version} + compile + true + + + + + org.ansj + ansj_seg + 5.1.6 + true + + + com.huaban + jieba-analysis + 1.0.2 + true + + + org.lionsoul + jcseg-core + 2.0.1 + true + + + com.chenlb.mmseg4j + mmseg4j-core + 1.10.0 + true + + + com.janeluo + ikanalyzer + 2012_u6 + true + + + org.apache.lucene + lucene-analyzers-common + + + org.apache.lucene + lucene-queryparser + + + org.apache.lucene + lucene-core + + + + + com.hankcs + hanlp + portable-1.7.2 + true + + + org.apache.lucene + lucene-analyzers-smartcn + 5.5.5 + true + + + org.apdplat + word + 1.2 + true + + + diff --git a/hutool-extra/src/main/java/cn/hutool/extra/emoji/EmojiUtil.java b/hutool-extra/src/main/java/cn/hutool/extra/emoji/EmojiUtil.java new file mode 100644 index 000000000..a163d6790 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/emoji/EmojiUtil.java @@ -0,0 +1,179 @@ +package cn.hutool.extra.emoji; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import com.vdurmont.emoji.Emoji; +import com.vdurmont.emoji.EmojiManager; +import com.vdurmont.emoji.EmojiParser; +import com.vdurmont.emoji.EmojiTrie; +import com.vdurmont.emoji.EmojiParser.FitzpatrickAction; + +/** + * 基于emoji-java的Emoji表情工具类 + *

+ * emoji-java文档以及别名列表见:https://github.com/vdurmont/emoji-java + * + * @author looly + * @since 4.2.1 + */ +public class EmojiUtil { + + /** + * 是否为Emoji表情的Unicode符 + * + * @param str 被测试的字符串 + * @return 是否为Emoji表情的Unicode符 + */ + public static boolean isEmoji(String str) { + return EmojiManager.isEmoji(str); + } + + /** + * 是否包含Emoji表情的Unicode符 + * + * @param str 被测试的字符串 + * @return 是否包含Emoji表情的Unicode符 + * @since 4.5.11 + */ + public static boolean containsEmoji(String str) { + if (str == null) { + return false; + } + final char[] chars = str.toCharArray(); + EmojiTrie.Matches status; + for (int i = 0; i < chars.length; i++) { + for (int j = i + 1; j <= chars.length; j++) { + status = EmojiManager.isEmoji(Arrays.copyOfRange(chars, i, j)); + if (status.impossibleMatch()) { + break; + } else if (status.exactMatch()) { + return true; + } + } + } + return false; + } + + /** + * 通过tag方式获取对应的所有Emoji表情 + * + * @param tag tag标签,例如“happy” + * @return Emoji表情集合,如果找不到返回null + */ + public static Set getByTag(String tag) { + return EmojiManager.getForTag(tag); + } + + /** + * 通过别名获取Emoji + * + * @param alias 别名,例如“smile” + * @return Emoji对象,如果找不到返回null + */ + public static Emoji get(String alias) { + return EmojiManager.getForAlias(alias); + } + + /** + * 将子串中的Emoji别名(两个":"包围的格式)和其HTML表示形式替换为为Unicode Emoji符号 + *

+ * 例如: + * + *

+	 *  :smile:  替换为 😄
+	 * &#128516; 替换为 😄
+	 * :boy|type_6: 替换为 👦🏿
+	 * 
+ * + * @param str 包含Emoji别名或者HTML表现形式的字符串 + * @return 替换后的字符串 + */ + public static String toUnicode(String str) { + return EmojiParser.parseToUnicode(str); + } + + /** + * 将字符串中的Unicode Emoji字符转换为别名表现形式(两个":"包围的格式) + *

+ * 例如: 😄 转换为 :smile: + * + *

+ * {@link FitzpatrickAction}参数被设置为{@link FitzpatrickAction#PARSE},则别名后会增加"|"并追加fitzpatrick类型 + *

+ * 例如:👦🏿 转换为 :boy|type_6: + * + *

+ * {@link FitzpatrickAction}参数被设置为{@link FitzpatrickAction#REMOVE},则别名后的"|"和类型将被去除 + *

+ * 例如:👦🏿 转换为 :boy: + * + *

+ * {@link FitzpatrickAction}参数被设置为{@link FitzpatrickAction#IGNORE},则别名后的类型将被忽略 + *

+ * 例如:👦🏿 转换为 :boy:🏿 + * + * @param str 包含Emoji Unicode字符的字符串 + * @return 替换后的字符串 + */ + public static String toAlias(String str) { + return toAlias(str, FitzpatrickAction.PARSE); + } + + /** + * 将字符串中的Unicode Emoji字符转换为别名表现形式(两个":"包围的格式),别名后会增加"|"并追加fitzpatrick类型 + *

+ * 例如:👦🏿 转换为 :boy|type_6: + * + * @param str 包含Emoji Unicode字符的字符串 + * @return 替换后的字符串 + */ + public static String toAlias(String str, FitzpatrickAction fitzpatrickAction) { + return EmojiParser.parseToAliases(str, fitzpatrickAction); + } + + /** + * 将字符串中的Unicode Emoji字符转换为HTML 16进制表现形式 + *

+ * 例如:👦🏿 转换为 &#x1f466; + * + * @param str 包含Emoji Unicode字符的字符串 + * @return 替换后的字符串 + */ + public static String toHtmlHex(String str) { + return EmojiParser.parseToHtmlHexadecimal(str); + } + + /** + * 将字符串中的Unicode Emoji字符转换为HTML表现形式 + *

+ * 例如:👦🏿 转换为 &#128102; + * + * @param str 包含Emoji Unicode字符的字符串 + * @return 替换后的字符串 + */ + public static String toHtml(String str) { + return EmojiParser.parseToHtmlHexadecimal(str); + } + + /** + * 去除字符串中所有的Emoji Unicode字符 + * + * @param str 包含Emoji字符的字符串 + * @return 替换后的字符串 + */ + public static String removeAllEmojis(String str) { + return EmojiParser.removeAllEmojis(str); + } + + /** + * 提取字符串中所有的Emoji Unicode + * + * @param str 包含Emoji字符的字符串 + * @return Emoji字符列表 + */ + public static List extractEmojis(String str) { + return EmojiParser.extractEmojis(str); + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/emoji/package-info.java b/hutool-extra/src/main/java/cn/hutool/extra/emoji/package-info.java new file mode 100644 index 000000000..dd352241d --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/emoji/package-info.java @@ -0,0 +1,7 @@ +/** + * 基于emoji-java的Emoji表情工具类 + * + * @author looly + * + */ +package cn.hutool.extra.emoji; \ No newline at end of file diff --git a/hutool-extra/src/main/java/cn/hutool/extra/ftp/AbstractFtp.java b/hutool-extra/src/main/java/cn/hutool/extra/ftp/AbstractFtp.java new file mode 100644 index 000000000..20377d7d3 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/ftp/AbstractFtp.java @@ -0,0 +1,175 @@ +package cn.hutool.extra.ftp; + +import java.io.Closeable; +import java.io.File; +import java.nio.charset.Charset; +import java.util.List; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 抽象FTP类,用于定义通用的FTP方法 + * + * @author looly + * @since 4.1.14 + */ +public abstract class AbstractFtp implements Closeable { + + public static final Charset DEFAULT_CHARSET = CharsetUtil.CHARSET_UTF_8 ; + + protected String host; + protected int port; + + protected String user; + protected String password; + + protected Charset charset; + + /** + * 如果连接超时的话,重新进行连接 + * @since 4.5.2 + * + * @return this + */ + public abstract AbstractFtp reconnectIfTimeout(); + + /** + * 打开指定目录 + * + * @param directory directory + * @return 是否打开目录 + */ + public abstract boolean cd(String directory); + + /** + * 打开上级目录 + * + * @return 是否打开目录 + * @since 4.0.5 + */ + public boolean toParent() { + return cd(".."); + } + + /** + * 远程当前目录(工作目录) + * + * @return 远程当前目录 + */ + public abstract String pwd(); + + /** + * 在当前远程目录(工作目录)下创建新的目录 + * + * @param dir 目录名 + * @return 是否创建成功 + */ + public abstract boolean mkdir(String dir); + + /** + * 文件或目录是否存在 + * + * @param path 目录 + * @return 是否存在 + */ + public boolean exist(String path) { + final String fileName = FileUtil.getName(path); + final String dir = StrUtil.removeSuffix(path, fileName); + final List names = ls(dir); + return containsIgnoreCase(names, fileName); + } + + /** + * 遍历某个目录下所有文件和目录,不会递归遍历 + * + * @param path 需要遍历的目录 + * @return 文件和目录列表 + */ + public abstract List ls(String path); + + /** + * 删除指定目录下的指定文件 + * + * @param path 目录路径 + * @return 是否存在 + */ + public abstract boolean delFile(String path); + + /** + * 删除文件夹及其文件夹下的所有文件 + * + * @param dirPath 文件夹路径 + * @return boolean 是否删除成功 + */ + public abstract boolean delDir(String dirPath); + + /** + * 创建指定文件夹及其父目录,从根目录开始创建,创建完成后回到默认的工作目录 + * + * @param dir 文件夹路径,绝对路径 + */ + public void mkDirs(String dir) { + final String[] dirs = StrUtil.trim(dir).split("[\\\\/]+"); + + final String now = pwd(); + if(dirs.length > 0 && StrUtil.isEmpty(dirs[0])) { + //首位为空,表示以/开头 + this.cd(StrUtil.SLASH); + } + for (int i = 0; i < dirs.length; i++) { + if (StrUtil.isNotEmpty(dirs[i])) { + if (false == cd(dirs[i])) { + //目录不存在时创建 + mkdir(dirs[i]); + cd(dirs[i]); + } + } + } + // 切换回工作目录 + cd(now); + } + + /** + * 将本地文件上传到目标服务器,目标文件名为destPath,若destPath为目录,则目标文件名将与srcFilePath文件名相同。覆盖模式 + * + * @param srcFilePath 本地文件路径 + * @param destFile 目标文件 + * @return 是否成功 + */ + public abstract boolean upload(String srcFilePath, File destFile); + + /** + * 下载文件 + * + * @param path 文件路径 + * @param outFile 输出文件或目录 + */ + public abstract void download(String path, File outFile); + + // ---------------------------------------------------------------------------------------------------------------------------------------- Private method start + /** + * 是否包含指定字符串,忽略大小写 + * + * @param names 文件或目录名列表 + * @param nameToFind 要查找的文件或目录名 + * @return 是否包含 + */ + private static boolean containsIgnoreCase(List names, String nameToFind) { + if (CollUtil.isEmpty(names)) { + return false; + } + if (StrUtil.isEmpty(nameToFind)) { + return false; + } + for (String name : names) { + if (nameToFind.equalsIgnoreCase(name)) { + return true; + } + } + return false; + } + // ---------------------------------------------------------------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/ftp/Ftp.java b/hutool-extra/src/main/java/cn/hutool/extra/ftp/Ftp.java new file mode 100644 index 000000000..2ce8c6d5f --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/ftp/Ftp.java @@ -0,0 +1,531 @@ +package cn.hutool.extra.ftp; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.net.ftp.FTPClient; +import org.apache.commons.net.ftp.FTPFile; +import org.apache.commons.net.ftp.FTPReply; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; + +/** + * FTP客户端封装
+ * 此客户端基于Apache-Commons-Net + * + * @author looly + * @since 4.1.8 + */ +public class Ftp extends AbstractFtp { + + /** 默认端口 */ + public static final int DEFAULT_PORT = 21; + + private FTPClient client; + private FtpMode mode; + /** 执行完操作是否返回当前目录 */ + private boolean backToPwd; + + /** + * 构造,匿名登录 + * + * @param host 域名或IP + */ + public Ftp(String host) { + this(host, DEFAULT_PORT); + } + + /** + * 构造,匿名登录 + * + * @param host 域名或IP + * @param port 端口 + */ + public Ftp(String host, int port) { + this(host, port, "anonymous", ""); + } + + /** + * 构造 + * + * @param host 域名或IP + * @param port 端口 + * @param user 用户名 + * @param password 密码 + */ + public Ftp(String host, int port, String user, String password) { + this(host, port, user, password, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 构造 + * + * @param host 域名或IP + * @param port 端口 + * @param user 用户名 + * @param password 密码 + * @param charset 编码 + */ + public Ftp(String host, int port, String user, String password, Charset charset) { + this(host, port, user, password, charset, null); + } + + /** + * 构造 + * + * @param host 域名或IP + * @param port 端口 + * @param user 用户名 + * @param password 密码 + * @param charset 编码 + * @param mode 模式 + */ + public Ftp(String host, int port, String user, String password, Charset charset, FtpMode mode) { + this.host = host; + this.port = port; + this.user = user; + this.password = password; + this.charset = charset; + this.mode = mode; + this.init(); + } + + /** + * 初始化连接 + * + * @return this + */ + public Ftp init() { + return this.init(this.host, this.port, this.user, this.password, this.mode); + } + + /** + * 初始化连接 + * + * @param host 域名或IP + * @param port 端口 + * @param user 用户名 + * @param password 密码 + * @return this + */ + public Ftp init(String host, int port, String user, String password) { + return this.init(host, port, user, password, null); + } + + /** + * 初始化连接 + * + * @param host 域名或IP + * @param port 端口 + * @param user 用户名 + * @param password 密码 + * @param mode 模式 + * @return this + */ + public Ftp init(String host, int port, String user, String password, FtpMode mode) { + final FTPClient client = new FTPClient(); + client.setControlEncoding(this.charset.toString()); + try { + // 连接ftp服务器 + client.connect(host, port); + // 登录ftp服务器 + client.login(user, password); + } catch (IOException e) { + throw new FtpException(e); + } + final int replyCode = client.getReplyCode(); // 是否成功登录服务器 + if (false == FTPReply.isPositiveCompletion(replyCode)) { + try { + client.disconnect(); + } catch (IOException e) { + // ignore + } + throw new FtpException("Login failed for user [{}], reply code is: [{}]", user, replyCode); + } + this.client = client; + if (mode != null) { + setMode(mode); + } + return this; + } + + /** + * 设置FTP连接模式,可选主动和被动模式 + * + * @param mode 模式枚举 + * @return this + * @since 4.1.19 + */ + public Ftp setMode(FtpMode mode) { + this.mode = mode; + switch (mode) { + case Active: + this.client.enterLocalActiveMode(); + break; + case Passive: + this.client.enterLocalPassiveMode(); + break; + } + return this; + } + + /** + * 设置执行完操作是否返回当前目录 + * + * @param backToPwd 执行完操作是否返回当前目录 + * @return this + * @since 4.6.0 + */ + public Ftp setBackToPwd(boolean backToPwd) { + this.backToPwd = backToPwd; + return this; + } + + /** + * 如果连接超时的话,重新进行连接 经测试,当连接超时时,client.isConnected()仍然返回ture,无法判断是否连接超时 因此,通过发送pwd命令的方式,检查连接是否超时 + * + * @return this + */ + @Override + public Ftp reconnectIfTimeout() { + String pwd = null; + try { + pwd = pwd(); + } catch (FtpException fex) { + // ignore + } + + if (pwd == null) { + return this.init(); + } + return this; + } + + /** + * 改变目录 + * + * @param directory 目录 + * @return 是否成功 + */ + @Override + public boolean cd(String directory) { + if (StrUtil.isBlank(directory)) { + return false; + } + + boolean flag = true; + try { + flag = client.changeWorkingDirectory(directory); + } catch (IOException e) { + throw new FtpException(e); + } + return flag; + } + + /** + * 远程当前目录 + * + * @return 远程当前目录 + * @since 4.1.14 + */ + @Override + public String pwd() { + try { + return client.printWorkingDirectory(); + } catch (IOException e) { + throw new FtpException(e); + } + } + + @Override + public List ls(String path) { + final FTPFile[] ftpFiles = lsFiles(path); + + final List fileNames = new ArrayList<>(); + for (FTPFile ftpFile : ftpFiles) { + fileNames.add(ftpFile.getName()); + } + return fileNames; + } + + /** + * 遍历某个目录下所有文件和目录,不会递归遍历 + * + * @param path 目录 + * @return 文件或目录列表 + */ + public FTPFile[] lsFiles(String path) { + String pwd = null; + if (StrUtil.isNotBlank(path)) { + pwd = pwd(); + cd(path); + } + + FTPFile[] ftpFiles; + try { + ftpFiles = this.client.listFiles(); + } catch (IOException e) { + throw new FtpException(e); + } finally { + // 回到原目录 + cd(pwd); + } + + return ftpFiles; + } + + @Override + public boolean mkdir(String dir) { + boolean flag = true; + try { + flag = this.client.makeDirectory(dir); + } catch (IOException e) { + throw new FtpException(e); + } + return flag; + } + + /** + * 判断ftp服务器文件是否存在 + * + * @param path 文件路径 + * @return 是否存在 + */ + public boolean existFile(String path) { + FTPFile[] ftpFileArr; + try { + ftpFileArr = client.listFiles(path); + } catch (IOException e) { + throw new FtpException(e); + } + if (ArrayUtil.isNotEmpty(ftpFileArr)) { + return true; + } + return false; + } + + @Override + public boolean delFile(String path) { + final String pwd = pwd(); + final String fileName = FileUtil.getName(path); + final String dir = StrUtil.removeSuffix(path, fileName); + cd(dir); + boolean isSuccess; + try { + isSuccess = client.deleteFile(fileName); + } catch (IOException e) { + throw new FtpException(e); + } finally { + // 回到原目录 + cd(pwd); + } + return isSuccess; + } + + @Override + public boolean delDir(String dirPath) { + FTPFile[] dirs; + try { + dirs = client.listFiles(dirPath); + } catch (IOException e) { + throw new FtpException(e); + } + String name; + String childPath; + for (FTPFile ftpFile : dirs) { + name = ftpFile.getName(); + childPath = StrUtil.format("{}/{}", dirPath, name); + if (ftpFile.isDirectory()) { + // 上级和本级目录除外 + if (false == name.equals(".") && false == name.equals("..")) { + delDir(childPath); + } + } else { + delFile(childPath); + } + } + + // 删除空目录 + try { + return this.client.removeDirectory(dirPath); + } catch (IOException e) { + throw new FtpException(e); + } + } + + /** + * 上传文件到指定目录,可选: + * + *

+	 * 1. path为null或""上传到当前路径
+	 * 2. path为相对路径则相对于当前路径的子路径
+	 * 3. path为绝对路径则上传到此路径
+	 * 
+ * + * @param path 服务端路径,可以为{@code null} 或者相对路径或绝对路径 + * @param file 文件 + * @return 是否上传成功 + */ + @Override + public boolean upload(String path, File file) { + Assert.notNull(file, "file to upload is null !"); + return upload(path, file.getName(), file); + } + + /** + * 上传文件到指定目录,可选: + * + *
+	 * 1. path为null或""上传到当前路径
+	 * 2. path为相对路径则相对于当前路径的子路径
+	 * 3. path为绝对路径则上传到此路径
+	 * 
+ * + * @param file 文件 + * @param path 服务端路径,可以为{@code null} 或者相对路径或绝对路径 + * @param fileName 自定义在服务端保存的文件名 + * @return 是否上传成功 + */ + public boolean upload(String path, String fileName, File file) { + try (InputStream in = FileUtil.getInputStream(file)) { + return upload(path, fileName, in); + } catch (IOException e) { + throw new FtpException(e); + } + } + + /** + * 上传文件到指定目录,可选: + * + *
+	 * 1. path为null或""上传到当前路径
+	 * 2. path为相对路径则相对于当前路径的子路径
+	 * 3. path为绝对路径则上传到此路径
+	 * 
+ * + * + * @param path 服务端路径,可以为{@code null} 或者相对路径或绝对路径 + * @param fileName 文件名 + * @param fileStream 文件流 + * @return 是否上传成功 + */ + public boolean upload(String path, String fileName, InputStream fileStream) { + try { + client.setFileType(FTPClient.BINARY_FILE_TYPE); + } catch (IOException e) { + throw new FtpException(e); + } + + String pwd = null; + if (this.backToPwd) { + pwd = pwd(); + } + + if (StrUtil.isNotBlank(path)) { + mkDirs(path); + boolean isOk = cd(path); + if (false == isOk) { + return false; + } + } + + try { + return client.storeFile(fileName, fileStream); + } catch (IOException e) { + throw new FtpException(e); + } finally { + if (this.backToPwd) { + cd(pwd); + } + } + } + + /** + * 下载文件 + * + * @param path 文件路径 + * @param outFile 输出文件或目录 + */ + @Override + public void download(String path, File outFile) { + final String fileName = FileUtil.getName(path); + final String dir = StrUtil.removeSuffix(path, fileName); + download(dir, fileName, outFile); + } + + /** + * 下载文件 + * + * @param path 文件路径 + * @param fileName 文件名 + * @param outFile 输出文件或目录 + */ + public void download(String path, String fileName, File outFile) { + if (outFile.isDirectory()) { + outFile = new File(outFile, fileName); + } + if (false == outFile.exists()) { + FileUtil.touch(outFile); + } + try (OutputStream out = FileUtil.getOutputStream(outFile)) { + download(path, fileName, out); + } catch (IOException e) { + throw new FtpException(e); + } + } + + /** + * 下载文件到输出流 + * + * @param path 文件路径 + * @param fileName 文件名 + * @param out 输出位置 + */ + public void download(String path, String fileName, OutputStream out) { + String pwd = null; + if (this.backToPwd) { + pwd = pwd(); + } + + cd(path); + try { + client.setFileType(FTPClient.BINARY_FILE_TYPE); + client.retrieveFile(fileName, out); + } catch (IOException e) { + throw new FtpException(e); + } finally { + if (backToPwd) { + cd(pwd); + } + } + } + + /** + * 获取FTPClient客户端对象 + * + * @return {@link FTPClient} + */ + public FTPClient getClient() { + return this.client; + } + + @Override + public void close() throws IOException { + if (null != this.client) { + this.client.logout(); + if (this.client.isConnected()) { + this.client.disconnect(); + } + this.client = null; + } + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/ftp/FtpException.java b/hutool-extra/src/main/java/cn/hutool/extra/ftp/FtpException.java new file mode 100644 index 000000000..72df46aa8 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/ftp/FtpException.java @@ -0,0 +1,33 @@ +package cn.hutool.extra.ftp; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.StrUtil; + +/** + * Ftp异常 + * + * @author xiaoleilu + */ +public class FtpException extends RuntimeException { + private static final long serialVersionUID = -8490149159895201756L; + + public FtpException(Throwable e) { + super(ExceptionUtil.getMessage(e), e); + } + + public FtpException(String message) { + super(message); + } + + public FtpException(String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params)); + } + + public FtpException(String message, Throwable throwable) { + super(message, throwable); + } + + public FtpException(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/ftp/FtpMode.java b/hutool-extra/src/main/java/cn/hutool/extra/ftp/FtpMode.java new file mode 100644 index 000000000..8e85da98c --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/ftp/FtpMode.java @@ -0,0 +1,17 @@ +package cn.hutool.extra.ftp; + +/** + * FTP连接模式 + * + *

+ * 见:https://www.cnblogs.com/huhaoshida/p/5412615.html + * + * @author looly + * @since 4.1.19 + */ +public enum FtpMode { + /** 主动模式 */ + Active, + /** 被动模式 */ + Passive; +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/ftp/package-info.java b/hutool-extra/src/main/java/cn/hutool/extra/ftp/package-info.java new file mode 100644 index 000000000..a302983c7 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/ftp/package-info.java @@ -0,0 +1,7 @@ +/** + * 基于Apache Commons Net封装的FTP工具 + * + * @author looly + * + */ +package cn.hutool.extra.ftp; \ No newline at end of file diff --git a/hutool-extra/src/main/java/cn/hutool/extra/mail/GlobalMailAccount.java b/hutool-extra/src/main/java/cn/hutool/extra/mail/GlobalMailAccount.java new file mode 100644 index 000000000..95797ef3b --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/mail/GlobalMailAccount.java @@ -0,0 +1,65 @@ +package cn.hutool.extra.mail; + +import cn.hutool.core.io.IORuntimeException; + +/** + * 全局邮件帐户,依赖于邮件配置文件{@link MailAccount#MAIL_SETTING_PATH}或{@link MailAccount#MAIL_SETTING_PATH2} + * + * @author looly + * + */ +public enum GlobalMailAccount { + INSTANCE; + + private final MailAccount mailAccount; + + /** + * 构造 + */ + private GlobalMailAccount() { + mailAccount = createDefaultAccount(); + } + + /** + * 获得邮件帐户 + * + * @return 邮件帐户 + */ + public MailAccount getAccount() { + return this.mailAccount; + } + + /** + * 创建默认帐户 + * + * @return MailAccount + */ + private MailAccount createDefaultAccount() { + MailAccount mailAccount = null; + try { + mailAccount = new MailAccount(MailAccount.MAIL_SETTING_PATH); + } catch (IORuntimeException e) { + //ignore + } + + // 寻找config/mailAccount.setting + if(null == mailAccount) { + try { + mailAccount = new MailAccount(MailAccount.MAIL_SETTING_PATH2); + } catch (IORuntimeException e) { + //ignore + } + } + + // 寻找mail.setting + if(null == mailAccount) { + try { + mailAccount = new MailAccount(MailAccount.MAIL_SETTING_PATH3); + } catch (IORuntimeException e) { + //ignore + } + } + + return mailAccount; + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/mail/InternalMailUtil.java b/hutool-extra/src/main/java/cn/hutool/extra/mail/InternalMailUtil.java new file mode 100644 index 000000000..e0a80ae64 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/mail/InternalMailUtil.java @@ -0,0 +1,108 @@ +package cn.hutool.extra.mail; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +import javax.mail.internet.AddressException; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeUtility; + +import cn.hutool.core.util.ArrayUtil; + +/** + * 邮件内部工具类 + * @author looly + * @since 3.2.3 + */ +public class InternalMailUtil { + + /** + * 将多个字符串邮件地址转为{@link InternetAddress}列表
+ * 单个字符串地址可以是多个地址合并的字符串 + * + * @param addrStrs 地址数组 + * @param charset 编码(主要用于中文用户名的编码) + * @return 地址数组 + * @since 4.0.3 + */ + public static InternetAddress[] parseAddressFromStrs(String[] addrStrs, Charset charset) { + final List resultList = new ArrayList<>(addrStrs.length); + InternetAddress[] addrs; + for (int i = 0; i < addrStrs.length; i++) { + addrs = parseAddress(addrStrs[i], charset); + if(ArrayUtil.isNotEmpty(addrs)) { + for(int j = 0 ; j < addrs.length; j++) { + resultList.add(addrs[j]); + } + } + } + return resultList.toArray(new InternetAddress[resultList.size()]); + } + + /** + * 解析第一个地址 + * + * @param address 地址字符串 + * @param charset 编码 + * @return 地址列表 + */ + public static InternetAddress parseFirstAddress(String address, Charset charset) { + final InternetAddress[] internetAddresses = parseAddress(address, charset); + if (ArrayUtil.isEmpty(internetAddresses)) { + try { + return new InternetAddress(address); + } catch (AddressException e) { + throw new MailException(e); + } + } + return internetAddresses[0]; + } + + /** + * 将一个地址字符串解析为多个地址
+ * 地址间使用" "、","、";"分隔 + * + * @param address 地址字符串 + * @param charset 编码 + * @return 地址列表 + */ + public static InternetAddress[] parseAddress(String address, Charset charset) { + InternetAddress[] addresses; + try { + addresses = InternetAddress.parse(address); + } catch (AddressException e) { + throw new MailException(e); + } + //编码用户名 + if (ArrayUtil.isNotEmpty(addresses)) { + for (InternetAddress internetAddress : addresses) { + try { + internetAddress.setPersonal(internetAddress.getPersonal(), charset.name()); + } catch (UnsupportedEncodingException e) { + throw new MailException(e); + } + } + } + + return addresses; + } + + /** + * 编码中文字符
+ * 编码失败返回原字符串 + * + * @param text 被编码的文本 + * @param charset 编码 + * @return 编码后的结果 + */ + public static String encodeText(String text, Charset charset) { + try { + return MimeUtility.encodeText(text, charset.name(), null); + } catch (UnsupportedEncodingException e) { + // ignore + } + return text; + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/mail/Mail.java b/hutool-extra/src/main/java/cn/hutool/extra/mail/Mail.java new file mode 100644 index 000000000..b225f299d --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/mail/Mail.java @@ -0,0 +1,354 @@ +package cn.hutool.extra.mail; + +import java.io.File; +import java.nio.charset.Charset; +import java.util.Date; + +import javax.activation.DataHandler; +import javax.activation.DataSource; +import javax.activation.FileDataSource; +import javax.mail.Authenticator; +import javax.mail.BodyPart; +import javax.mail.MessagingException; +import javax.mail.Multipart; +import javax.mail.Session; +import javax.mail.Transport; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 邮件发送客户端 + * + * @author looly + * @since 3.2.0 + */ +public class Mail { + + /** 邮箱帐户信息以及一些客户端配置信息 */ + private MailAccount mailAccount; + /** 收件人列表 */ + private String[] tos; + /** 抄送人列表(carbon copy) */ + private String[] ccs; + /** 密送人列表(blind carbon copy) */ + private String[] bccs; + /** 回复地址(reply-to) */ + private String[] reply; + /** 标题 */ + private String title; + /** 内容 */ + private String content; + /** 是否为HTML */ + private boolean isHtml; + /** 附件列表 */ + private DataSource[] attachments; + /** 是否使用全局会话,默认为false */ + private boolean useGlobalSession = false; + + /** + * 创建邮件客户端 + * + * @param mailAccount 邮件帐号 + * @return {@link Mail} + */ + public static Mail create(MailAccount mailAccount) { + return new Mail(mailAccount); + } + + /** + * 创建邮件客户端,使用全局邮件帐户 + * + * @return {@link Mail} + */ + public static Mail create() { + return new Mail(); + } + + // --------------------------------------------------------------- Constructor start + /** + * 构造,使用全局邮件帐户 + */ + public Mail() { + this(GlobalMailAccount.INSTANCE.getAccount()); + } + + /** + * 构造 + * + * @param mailAccount 邮件帐户,如果为null使用默认配置文件的全局邮件配置 + */ + public Mail(MailAccount mailAccount) { + mailAccount = (null != mailAccount) ? mailAccount : GlobalMailAccount.INSTANCE.getAccount(); + this.mailAccount = mailAccount.defaultIfEmpty(); + } + // --------------------------------------------------------------- Constructor end + + // --------------------------------------------------------------- Getters and Setters start + /** + * 设置收件人 + * + * @param tos 收件人列表 + * @return this + * @see #setTos(String...) + */ + public Mail to(String... tos) { + return setTos(tos); + } + + /** + * 设置多个收件人 + * + * @param tos 收件人列表 + * @return this + */ + public Mail setTos(String... tos) { + this.tos = tos; + return this; + } + + /** + * 设置多个抄送人(carbon copy) + * + * @param ccs 抄送人列表 + * @return this + * @since 4.0.3 + */ + public Mail setCcs(String... ccs) { + this.ccs = ccs; + return this; + } + + /** + * 设置多个密送人(blind carbon copy) + * + * @param bccs 密送人列表 + * @return this + * @since 4.0.3 + */ + public Mail setBccs(String... bccs) { + this.bccs = bccs; + return this; + } + + /** + * 设置多个回复地址(reply-to) + * + * @param reply 回复地址(reply-to)列表 + * @return this + * @since 4.6.0 + */ + public Mail setReply(String... reply) { + this.reply = reply; + return this; + } + + /** + * 设置标题 + * + * @param title 标题 + * @return this + */ + public Mail setTitle(String title) { + this.title = title; + return this; + } + + /** + * 设置正文 + * + * @param content 正文 + * @return this + */ + public Mail setContent(String content) { + this.content = content; + return this; + } + + /** + * 设置是否是HTML + * + * @param isHtml 是否为HTML + * @return this + */ + public Mail setHtml(boolean isHtml) { + this.isHtml = isHtml; + return this; + } + + /** + * 设置文件类型附件 + * + * @param files 附件文件列表 + * @return this + */ + public Mail setFiles(File... files) { + if(ArrayUtil.isEmpty(files)) { + return this; + } + + final DataSource[] attachments = new DataSource[files.length]; + for (int i = 0; i < files.length; i++) { + attachments[i] = new FileDataSource(files[i]); + } + return setAttachments(attachments); + } + + /** + * 设置附件,附件使用{@link DataSource} 形式表示,可以使用{@link FileDataSource}包装文件表示文件附件 + * + * @param attachments 附件列表 + * @return this + * @since 4.0.9 + */ + public Mail setAttachments(DataSource... attachments) { + if(ArrayUtil.isNotEmpty(attachments)) { + this.attachments = attachments; + } + return this; + } + + /** + * 设置字符集编码 + * + * @param charset 字符集编码 + * @return this + * @see MailAccount#setCharset(Charset) + */ + public Mail setCharset(Charset charset) { + this.mailAccount.setCharset(charset); + return this; + } + + /** + * 设置是否使用全局会话,默认为true + * + * @param isUseGlobalSession 是否使用全局会话,默认为true + * @return this + * @since 4.0.2 + */ + public Mail setUseGlobalSession(boolean isUseGlobalSession) { + this.useGlobalSession = isUseGlobalSession; + return this; + } + // --------------------------------------------------------------- Getters and Setters end + + /** + * 发送 + * + * @return this + * @throws MailException 邮件发送异常 + */ + public Mail send() throws MailException { + try { + return doSend(); + } catch (MessagingException e) { + throw new MailException(e); + } + } + + // --------------------------------------------------------------- Private method start + /** + * 执行发送 + * + * @return this + * @throws MessagingException 发送异常 + */ + private Mail doSend() throws MessagingException { + Transport.send(buildMsg()); + return this; + } + + /** + * 构建消息 + * + * @return {@link MimeMessage}消息 + * @throws MessagingException 消息异常 + */ + private MimeMessage buildMsg() throws MessagingException { + final Charset charset = this.mailAccount.getCharset(); + final MimeMessage msg = new MimeMessage(getSession(this.useGlobalSession)); + // 发件人 + final String from = this.mailAccount.getFrom(); + if (StrUtil.isEmpty(from)) { + // 用户未提供发送方,则从Session中自动获取 + msg.setFrom(); + } else { + msg.setFrom(InternalMailUtil.parseFirstAddress(from, charset)); + } + // 标题 + msg.setSubject(this.title, charset.name()); + // 发送时间 + msg.setSentDate(new Date()); + // 内容和附件 + msg.setContent(buildContent(charset)); + // 收件人 + msg.setRecipients(MimeMessage.RecipientType.TO, InternalMailUtil.parseAddressFromStrs(this.tos, charset)); + // 抄送人 + if (ArrayUtil.isNotEmpty(this.ccs)) { + msg.setRecipients(MimeMessage.RecipientType.CC, InternalMailUtil.parseAddressFromStrs(this.ccs, charset)); + } + // 密送人 + if (ArrayUtil.isNotEmpty(this.bccs)) { + msg.setRecipients(MimeMessage.RecipientType.BCC, InternalMailUtil.parseAddressFromStrs(this.bccs, charset)); + } + // 回复地址(reply-to) + if (ArrayUtil.isNotEmpty(this.reply)) { + msg.setReplyTo(InternalMailUtil.parseAddressFromStrs(this.reply, charset)); + } + + return msg; + } + + /** + * 构建邮件信息主体 + * + * @param charset 编码 + * @return 邮件信息主体 + * @throws MessagingException 消息异常 + */ + private Multipart buildContent(Charset charset) throws MessagingException { + final Multipart mainPart = new MimeMultipart(); + + // 正文 + final BodyPart body = new MimeBodyPart(); + body.setContent(content, StrUtil.format("text/{}; charset={}", isHtml ? "html" : "plain", charset)); + mainPart.addBodyPart(body); + + // 附件 + if (ArrayUtil.isNotEmpty(this.attachments)) { + BodyPart bodyPart; + for (DataSource attachment : attachments) { + bodyPart = new MimeBodyPart(); + bodyPart.setDataHandler(new DataHandler(attachment)); + bodyPart.setFileName(InternalMailUtil.encodeText(attachment.getName(), charset)); + mainPart.addBodyPart(bodyPart); + } + } + + return mainPart; + } + + /** + * 获取默认邮件会话
+ * 如果为全局单例的会话,则全局只允许一个邮件帐号,否则每次发送邮件会新建一个新的会话 + * + * @param isSingleton 是否使用单例Session + * @return 邮件会话 {@link Session} + * @since 4.0.2 + */ + private Session getSession(boolean isSingleton) { + final MailAccount mailAccount = this.mailAccount; + Authenticator authenticator = null; + if (mailAccount.isAuth()) { + authenticator = new UserPassAuthenticator(mailAccount.getUser(), mailAccount.getPass()); + } + + return isSingleton ? Session.getDefaultInstance(mailAccount.getSmtpProps(), authenticator) // + : Session.getInstance(mailAccount.getSmtpProps(), authenticator); + } + // --------------------------------------------------------------- Private method end +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/mail/MailAccount.java b/hutool-extra/src/main/java/cn/hutool/extra/mail/MailAccount.java new file mode 100644 index 000000000..25bf2a34b --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/mail/MailAccount.java @@ -0,0 +1,489 @@ +package cn.hutool.extra.mail; + +import java.io.Serializable; +import java.nio.charset.Charset; +import java.util.Properties; + +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.setting.Setting; + +/** + * 邮件账户对象 + * + * @author Luxiaolei + * + */ +public class MailAccount implements Serializable { + private static final long serialVersionUID = -6937313421815719204L; + + private static final String MAIL_PROTOCOL = "mail.transport.protocol"; + private static final String SMTP_HOST = "mail.smtp.host"; + private static final String SMTP_PORT = "mail.smtp.port"; + private static final String SMTP_AUTH = "mail.smtp.auth"; + private static final String SMTP_CONNECTION_TIMEOUT = "mail.smtp.connectiontimeout"; + private static final String SMTP_TIMEOUT = "mail.smtp.timeout"; + + private static final String STARTTTLS_ENABLE = "mail.smtp.starttls.enable"; + private static final String SOCKEY_FACTORY = "mail.smtp.socketFactory.class"; + private static final String SOCKEY_FACTORY_FALLBACK = "mail.smtp.socketFactory.fallback"; + private static final String SOCKEY_FACTORY_PORT = "smtp.socketFactory.port"; + + private static final String MAIL_DEBUG = "mail.debug"; + private static final String SPLIT_LONG_PARAMS = "mail.mime.splitlongparameters"; + + public static final String MAIL_SETTING_PATH = "config/mail.setting"; + public static final String MAIL_SETTING_PATH2 = "config/mailAccount.setting"; + public static final String MAIL_SETTING_PATH3 = "mail.setting"; + + /** SMTP服务器域名 */ + private String host; + /** SMTP服务端口 */ + private Integer port; + /** 是否需要用户名密码验证 */ + private Boolean auth; + /** 用户名 */ + private String user; + /** 密码 */ + private String pass; + /** 发送方,遵循RFC-822标准 */ + private String from; + + /** 是否打开调试模式,调试模式会显示与邮件服务器通信过程,默认不开启 */ + private boolean debug; + /** 编码用于编码邮件正文和发送人、收件人等中文 */ + private Charset charset = CharsetUtil.CHARSET_UTF_8; + /** 对于超长参数是否切分为多份,默认为false(国内邮箱附件不支持切分的附件名) */ + private boolean splitlongparameters; + + /** 使用 STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展。它将纯文本连接升级为加密连接(TLS或SSL), 而不是使用一个单独的加密通信端口。 */ + private boolean startttlsEnable = false; + /** 使用 SSL安全连接 */ + private Boolean sslEnable; + /** 指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字 */ + private String socketFactoryClass = "javax.net.ssl.SSLSocketFactory"; + /** 如果设置为true,未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true */ + private boolean socketFactoryFallback; + /** 指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口 */ + private int socketFactoryPort = 465; + + /** SMTP超时时长,单位毫秒,缺省值不超时 */ + private long timeout; + /** Socket连接超时值,单位毫秒,缺省值不超时 */ + private long connectionTimeout; + + // -------------------------------------------------------------- Constructor start + /** + * 构造,所有参数需自行定义或保持默认值 + */ + public MailAccount() { + } + + /** + * 构造 + * + * @param settingPath 配置文件路径 + */ + public MailAccount(String settingPath) { + this(new Setting(settingPath)); + } + + /** + * 构造 + * + * @param setting 配置文件 + */ + public MailAccount(Setting setting) { + setting.toBean(this); + } + + // -------------------------------------------------------------- Constructor end + /** + * 获得SMTP服务器域名 + * + * @return SMTP服务器域名 + */ + public String getHost() { + return host; + } + + /** + * 设置SMTP服务器域名 + * + * @param host SMTP服务器域名 + * @return this + */ + public MailAccount setHost(String host) { + this.host = host; + return this; + } + + /** + * 获得SMTP服务端口 + * + * @return SMTP服务端口 + */ + public Integer getPort() { + return port; + } + + /** + * 设置SMTP服务端口 + * + * @param port SMTP服务端口 + * @return this + */ + public MailAccount setPort(Integer port) { + this.port = port; + return this; + } + + /** + * 是否需要用户名密码验证 + * + * @return 是否需要用户名密码验证 + */ + public Boolean isAuth() { + return auth; + } + + /** + * 设置是否需要用户名密码验证 + * + * @param isAuth 是否需要用户名密码验证 + * @return this + */ + public MailAccount setAuth(boolean isAuth) { + this.auth = isAuth; + return this; + } + + /** + * 获取用户名 + * + * @return 用户名 + */ + public String getUser() { + return user; + } + + /** + * 设置用户名 + * + * @param user 用户名 + * @return this + */ + public MailAccount setUser(String user) { + this.user = user; + return this; + } + + /** + * 获取密码 + * + * @return 密码 + */ + public String getPass() { + return pass; + } + + /** + * 设置密码 + * + * @param pass 密码 + * @return this + */ + public MailAccount setPass(String pass) { + this.pass = pass; + return this; + } + + /** + * 获取发送方,遵循RFC-822标准 + * + * @return 发送方,遵循RFC-822标准 + */ + public String getFrom() { + return from; + } + + /** + * 设置发送方,遵循RFC-822标准
+ * 发件人可以是以下形式: + * + *

+	 * 1. user@xxx.xx
+	 * 2.  name 
+	 * 
+ * + * @param from 发送方,遵循RFC-822标准 + * @return this + */ + public MailAccount setFrom(String from) { + this.from = from; + return this; + } + + /** + * 是否打开调试模式,调试模式会显示与邮件服务器通信过程,默认不开启 + * + * @return 是否打开调试模式,调试模式会显示与邮件服务器通信过程,默认不开启 + * @since 4.0.2 + */ + public boolean isDebug() { + return debug; + } + + /** + * 设置是否打开调试模式,调试模式会显示与邮件服务器通信过程,默认不开启 + * + * @param debug 是否打开调试模式,调试模式会显示与邮件服务器通信过程,默认不开启 + * @since 4.0.2 + */ + public MailAccount setDebug(boolean debug) { + this.debug = debug; + return this; + } + + /** + * 获取字符集编码 + * + * @return 编码 + */ + public Charset getCharset() { + return charset; + } + + /** + * 设置字符集编码 + * + * @param charset 字符集编码 + * @return this + */ + public MailAccount setCharset(Charset charset) { + this.charset = charset; + return this; + } + + /** + * 对于超长参数是否切分为多份,默认为false(国内邮箱附件不支持切分的附件名) + * + * @return 对于超长参数是否切分为多份 + */ + public boolean isSplitlongparameters() { + return splitlongparameters; + } + + /** + * 设置对于超长参数是否切分为多份,默认为false(国内邮箱附件不支持切分的附件名) + * + * @param splitlongparameters 对于超长参数是否切分为多份 + */ + public void setSplitlongparameters(boolean splitlongparameters) { + this.splitlongparameters = splitlongparameters; + } + + /** + * 是否使用 STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展。它将纯文本连接升级为加密连接(TLS或SSL), 而不是使用一个单独的加密通信端口。 + * + * @return 是否使用 STARTTLS安全连接 + */ + public boolean isStartttlsEnable() { + return this.startttlsEnable; + } + + /** + * 设置是否使用STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展。它将纯文本连接升级为加密连接(TLS或SSL), 而不是使用一个单独的加密通信端口。 + * + * @param startttlsEnable 是否使用STARTTLS安全连接 + * @return this + */ + public MailAccount setStartttlsEnable(boolean startttlsEnable) { + this.startttlsEnable = startttlsEnable; + return this; + } + + /** + * 是否使用 SSL安全连接 + * + * @return 是否使用 SSL安全连接 + */ + public Boolean isSslEnable() { + return this.sslEnable; + } + + /** + * 设置是否使用SSL安全连接 + * + * @param sslEnable 是否使用SSL安全连接 + * @return this + */ + public MailAccount setSslEnable(Boolean sslEnable) { + this.sslEnable = sslEnable; + return this; + } + + /** + * 获取指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字 + * + * @return 指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字 + */ + public String getSocketFactoryClass() { + return socketFactoryClass; + } + + /** + * 设置指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字 + * + * @param socketFactoryClass 指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字 + * @return this + */ + public MailAccount setSocketFactoryClass(String socketFactoryClass) { + this.socketFactoryClass = socketFactoryClass; + return this; + } + + /** + * 如果设置为true,未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true + * + * @return 如果设置为true,未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true + */ + public boolean isSocketFactoryFallback() { + return socketFactoryFallback; + } + + /** + * 如果设置为true,未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true + * + * @param socketFactoryFallback 如果设置为true,未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true + * @return this + */ + public MailAccount setSocketFactoryFallback(boolean socketFactoryFallback) { + this.socketFactoryFallback = socketFactoryFallback; + return this; + } + + /** + * 获取指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口 + * + * @return 指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口 + */ + public int getSocketFactoryPort() { + return socketFactoryPort; + } + + /** + * 指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口 + * + * @param socketFactoryPort 指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口 + * @return this + */ + public MailAccount setSocketFactoryPort(int socketFactoryPort) { + this.socketFactoryPort = socketFactoryPort; + return this; + } + + /** + * 设置SMTP超时时长,单位毫秒,缺省值不超时 + * @param timeout SMTP超时时长,单位毫秒,缺省值不超时 + * @return this + * @since 4.1.17 + */ + public MailAccount setTimeout(long timeout) { + this.timeout = timeout; + return this; + } + + /** + * 设置Socket连接超时值,单位毫秒,缺省值不超时 + * @param connectionTimeout Socket连接超时值,单位毫秒,缺省值不超时 + * @return this + * @since 4.1.17 + */ + public MailAccount setConnectionTimeout(long connectionTimeout) { + this.connectionTimeout = connectionTimeout; + return this; + } + + /** + * 获得SMTP相关信息 + * + * @return {@link Properties} + */ + public Properties getSmtpProps() { + //全局系统参数 + System.setProperty(SPLIT_LONG_PARAMS, String.valueOf(this.splitlongparameters)); + + final Properties p = new Properties(); + p.put(MAIL_PROTOCOL, "smtp"); + p.put(SMTP_HOST, this.host); + p.put(SMTP_PORT, String.valueOf(this.port)); + p.put(SMTP_AUTH, String.valueOf(this.auth)); + if(this.timeout > 0) { + p.put(SMTP_TIMEOUT, String.valueOf(this.timeout)); + } + if(this.connectionTimeout > 0) { + p.put(SMTP_CONNECTION_TIMEOUT, String.valueOf(this.connectionTimeout)); + } + + p.put(MAIL_DEBUG, String.valueOf(this.debug)); + + if (this.startttlsEnable) { + //STARTTLS是对纯文本通信协议的扩展。它将纯文本连接升级为加密连接(TLS或SSL), 而不是使用一个单独的加密通信端口。 + p.put(STARTTTLS_ENABLE, String.valueOf(this.startttlsEnable)); + + if(null == this.sslEnable) { + //为了兼容旧版本,当用户没有此项配置时,按照startttlsEnable开启状态时对待 + this.sslEnable = true; + } + } + + // SSL + if(null != this.sslEnable && this.sslEnable) { + p.put(SOCKEY_FACTORY, socketFactoryClass); + p.put(SOCKEY_FACTORY_FALLBACK, String.valueOf(this.socketFactoryFallback)); + p.put(SOCKEY_FACTORY_PORT, String.valueOf(this.socketFactoryPort)); + } + + return p; + } + + /** + * 如果某些值为null,使用默认值 + * + * @return this + */ + public MailAccount defaultIfEmpty() { + // 去掉发件人的姓名部分 + final String fromAddress = InternalMailUtil.parseFirstAddress(this.from, this.charset).getAddress(); + + if (StrUtil.isBlank(this.host)) { + // 如果SMTP地址为空,默认使用smtp.<发件人邮箱后缀> + this.host = StrUtil.format("smtp.{}", StrUtil.subSuf(fromAddress, fromAddress.indexOf('@') + 1)); + } + if (StrUtil.isBlank(user)) { + // 如果用户名为空,默认为发件人邮箱前缀 + this.user = StrUtil.subPre(fromAddress, fromAddress.indexOf('@')); + } + if (null == this.auth) { + // 如果密码非空白,则使用认证模式 + this.auth = (false == StrUtil.isBlank(this.pass)); + } + if (null == this.port) { + // 端口在SSL状态下默认与socketFactoryPort一致,非SSL状态下默认为25 + this.port = (null != this.sslEnable && this.sslEnable) ? this.socketFactoryPort : 25; + } + if (null == this.charset) { + // 默认UTF-8编码 + this.charset = CharsetUtil.CHARSET_UTF_8; + } + + return this; + } + + @Override + public String toString() { + return "MailAccount [host=" + host + ", port=" + port + ", auth=" + auth + ", user=" + user + ", pass=" + (StrUtil.isEmpty(this.pass) ? "" : "******") + ", from=" + from + ", startttlsEnable=" + + startttlsEnable + ", socketFactoryClass=" + socketFactoryClass + ", socketFactoryFallback=" + socketFactoryFallback + ", socketFactoryPort=" + socketFactoryPort + "]"; + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/mail/MailException.java b/hutool-extra/src/main/java/cn/hutool/extra/mail/MailException.java new file mode 100644 index 000000000..8b78e023b --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/mail/MailException.java @@ -0,0 +1,32 @@ +package cn.hutool.extra.mail; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 邮件异常 + * @author xiaoleilu + */ +public class MailException extends RuntimeException{ + private static final long serialVersionUID = 8247610319171014183L; + + public MailException(Throwable e) { + super(ExceptionUtil.getMessage(e), e); + } + + public MailException(String message) { + super(message); + } + + public MailException(String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params)); + } + + public MailException(String message, Throwable throwable) { + super(message, throwable); + } + + public MailException(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/mail/MailUtil.java b/hutool-extra/src/main/java/cn/hutool/extra/mail/MailUtil.java new file mode 100644 index 000000000..bb54fd5f3 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/mail/MailUtil.java @@ -0,0 +1,236 @@ +package cn.hutool.extra.mail; + +import java.io.File; +import java.util.Collection; +import java.util.List; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 邮件工具类,基于javax.mail封装 + * + * @author looly + * @since 3.1.2 + */ +public class MailUtil { + + /** + * 使用配置文件中设置的账户发送文本邮件,发送给单个或多个收件人
+ * 多个收件人可以使用逗号“,”分隔,也可以通过分号“;”分隔 + * + * @param to 收件人 + * @param subject 标题 + * @param content 正文 + * @param files 附件列表 + * @since 3.2.0 + */ + public static void sendText(String to, String subject, String content, File... files) { + send(to, subject, content, false, files); + } + + /** + * 使用配置文件中设置的账户发送HTML邮件,发送给单个或多个收件人
+ * 多个收件人可以使用逗号“,”分隔,也可以通过分号“;”分隔 + * + * @param to 收件人 + * @param subject 标题 + * @param content 正文 + * @param files 附件列表 + * @since 3.2.0 + */ + public static void sendHtml(String to, String subject, String content, File... files) { + send(to, subject, content, true, files); + } + + /** + * 使用配置文件中设置的账户发送邮件,发送单个或多个收件人
+ * 多个收件人可以使用逗号“,”分隔,也可以通过分号“;”分隔 + * + * @param to 收件人 + * @param subject 标题 + * @param content 正文 + * @param isHtml 是否为HTML + * @param files 附件列表 + */ + public static void send(String to, String subject, String content, boolean isHtml, File... files) { + send(splitAddress(to), subject, content, isHtml, files); + } + + /** + * 使用配置文件中设置的账户发送邮件,发送单个或多个收件人
+ * 多个收件人、抄送人、密送人可以使用逗号“,”分隔,也可以通过分号“;”分隔 + * + * @param to 收件人,可以使用逗号“,”分隔,也可以通过分号“;”分隔 + * @param cc 抄送人,可以使用逗号“,”分隔,也可以通过分号“;”分隔 + * @param bcc 密送人,可以使用逗号“,”分隔,也可以通过分号“;”分隔 + * @param subject 标题 + * @param content 正文 + * @param isHtml 是否为HTML + * @param files 附件列表 + * @since 4.0.3 + */ + public static void send(String to, String cc, String bcc, String subject, String content, boolean isHtml, File... files) { + send(splitAddress(to), splitAddress(cc), splitAddress(bcc), subject, content, isHtml, files); + } + + /** + * 使用配置文件中设置的账户发送文本邮件,发送给多人 + * + * @param tos 收件人列表 + * @param subject 标题 + * @param content 正文 + * @param files 附件列表 + */ + public static void sendText(Collection tos, String subject, String content, File... files) { + send(tos, subject, content, false, files); + } + + /** + * 使用配置文件中设置的账户发送HTML邮件,发送给多人 + * + * @param tos 收件人列表 + * @param subject 标题 + * @param content 正文 + * @param files 附件列表 + * @since 3.2.0 + */ + public static void sendHtml(Collection tos, String subject, String content, File... files) { + send(tos, subject, content, true, files); + } + + /** + * 使用配置文件中设置的账户发送邮件,发送给多人 + * + * @param tos 收件人列表 + * @param subject 标题 + * @param content 正文 + * @param isHtml 是否为HTML + * @param files 附件列表 + */ + public static void send(Collection tos, String subject, String content, boolean isHtml, File... files) { + send(tos, null, null, subject, content, isHtml, files); + } + + /** + * 使用配置文件中设置的账户发送邮件,发送给多人 + * + * @param tos 收件人列表 + * @param ccs 抄送人列表,可以为null或空 + * @param bccs 密送人列表,可以为null或空 + * @param subject 标题 + * @param content 正文 + * @param isHtml 是否为HTML + * @param files 附件列表 + * @since 4.0.3 + */ + public static void send(Collection tos, Collection ccs, Collection bccs, String subject, String content, boolean isHtml, File... files) { + send(GlobalMailAccount.INSTANCE.getAccount(), true, tos, ccs, bccs, subject, content, isHtml, files); + } + + //------------------------------------------------------------------------------------------------------------------------------- Custom MailAccount + /** + * 发送邮件给多人 + * + * @param mailAccount 邮件认证对象 + * @param to 收件人,多个收件人逗号或者分号隔开 + * @param subject 标题 + * @param content 正文 + * @param isHtml 是否为HTML格式 + * @param files 附件列表 + * @since 3.2.0 + */ + public static void send(MailAccount mailAccount, String to, String subject, String content, boolean isHtml, File... files) { + send(mailAccount, splitAddress(to), subject, content, isHtml, files); + } + + /** + * 发送邮件给多人 + * + * @param mailAccount 邮件帐户信息 + * @param tos 收件人列表 + * @param subject 标题 + * @param content 正文 + * @param isHtml 是否为HTML格式 + * @param files 附件列表 + */ + public static void send(MailAccount mailAccount, Collection tos, String subject, String content, boolean isHtml, File... files) { + send(mailAccount, tos, null, null, subject, content, isHtml, files); + } + + /** + * 发送邮件给多人 + * + * @param mailAccount 邮件帐户信息 + * @param tos 收件人列表 + * @param ccs 抄送人列表,可以为null或空 + * @param bccs 密送人列表,可以为null或空 + * @param subject 标题 + * @param content 正文 + * @param isHtml 是否为HTML格式 + * @param files 附件列表 + * @since 4.0.3 + */ + public static void send(MailAccount mailAccount, Collection tos, Collection ccs, Collection bccs, String subject, String content, boolean isHtml, File... files) { + send(mailAccount, false, tos, ccs, bccs, subject, content, isHtml, files); + } + + //------------------------------------------------------------------------------------------------------------------------ Private method start + /** + * 发送邮件给多人 + * + * @param mailAccount 邮件帐户信息 + * @param useGlobalSession 是否全局共享Session + * @param tos 收件人列表 + * @param ccs 抄送人列表,可以为null或空 + * @param bccs 密送人列表,可以为null或空 + * @param subject 标题 + * @param content 正文 + * @param isHtml 是否为HTML格式 + * @param files 附件列表 + * @since 4.0.3 + */ + private static void send(MailAccount mailAccount, boolean useGlobalSession, Collection tos, Collection ccs, Collection bccs, String subject, String content, boolean isHtml, File... files) { + final Mail mail = Mail.create(mailAccount).setUseGlobalSession(useGlobalSession); + + //可选抄送人 + if(CollUtil.isNotEmpty(ccs)) { + mail.setCcs(ccs.toArray(new String[ccs.size()])); + } + //可选密送人 + if(CollUtil.isNotEmpty(bccs)) { + mail.setBccs(bccs.toArray(new String[bccs.size()])); + } + + mail.setTos(tos.toArray(new String[tos.size()])); + mail.setTitle(subject); + mail.setContent(content); + mail.setHtml(isHtml); + mail.setFiles(files); + + mail.send(); + } + + /** + * 将多个联系人转为列表,分隔符为逗号或者分号 + * + * @param addresses 多个联系人,如果为空返回null + * @return 联系人列表 + */ + private static List splitAddress(String addresses){ + if(StrUtil.isBlank(addresses)) { + return null; + } + + List result; + if(StrUtil.contains(addresses, ',')) { + result = StrUtil.splitTrim(addresses, ','); + }else if(StrUtil.contains(addresses, ';')) { + result = StrUtil.splitTrim(addresses, ';'); + }else { + result = CollUtil.newArrayList(addresses); + } + return result; + } + //------------------------------------------------------------------------------------------------------------------------ Private method end +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/mail/UserPassAuthenticator.java b/hutool-extra/src/main/java/cn/hutool/extra/mail/UserPassAuthenticator.java new file mode 100644 index 000000000..3cd25a2fe --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/mail/UserPassAuthenticator.java @@ -0,0 +1,34 @@ +package cn.hutool.extra.mail; + +import javax.mail.Authenticator; +import javax.mail.PasswordAuthentication; + +/** + * 用户名密码验证器 + * + * @author looly + * @since 3.1.2 + */ +public class UserPassAuthenticator extends Authenticator { + + private String user; + private String pass; + + /** + * 构造 + * + * @param user 用户名 + * @param pass 密码 + */ + public UserPassAuthenticator(String user, String pass) { + super(); + this.user = user; + this.pass = pass; + } + + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(this.user, this.pass); + } + +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/mail/package-info.java b/hutool-extra/src/main/java/cn/hutool/extra/mail/package-info.java new file mode 100644 index 000000000..638a6cd68 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/mail/package-info.java @@ -0,0 +1,7 @@ +/** + * 邮件封装,基于javax-mail库,入口为MailUtil + * + * @author looly + * + */ +package cn.hutool.extra.mail; \ No newline at end of file diff --git a/hutool-extra/src/main/java/cn/hutool/extra/package-info.java b/hutool-extra/src/main/java/cn/hutool/extra/package-info.java new file mode 100644 index 000000000..e5210ccf0 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/package-info.java @@ -0,0 +1,7 @@ +/** + * 由于Hutool的原则是不依赖于其它配置文件,但是很多时候我们需要针对第三方非常棒的库做一些工具类化的支持,因此Hutoo-extra包主要用于支持第三方库的工具类支持。 + * + * @author looly + * + */ +package cn.hutool.extra; \ No newline at end of file diff --git a/hutool-extra/src/main/java/cn/hutool/extra/qrcode/BufferedImageLuminanceSource.java b/hutool-extra/src/main/java/cn/hutool/extra/qrcode/BufferedImageLuminanceSource.java new file mode 100644 index 000000000..50f2fcad3 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/qrcode/BufferedImageLuminanceSource.java @@ -0,0 +1,120 @@ +package cn.hutool.extra.qrcode; + +import com.google.zxing.LuminanceSource; + +import java.awt.Graphics2D; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; + +/** + * {@link BufferedImage} 图片二维码源
+ * 来自:http://blog.csdn.net/yangxin_blog/article/details/50850701
+ * 此类同样在zxing-j2se包中也有提供 + * + * @author zxing, Looly + * @since 4.0.2 + */ +public final class BufferedImageLuminanceSource extends LuminanceSource { + + private final BufferedImage image; + private final int left; + private final int top; + + /** + * 构造 + * + * @param image {@link BufferedImage} + */ + public BufferedImageLuminanceSource(BufferedImage image) { + this(image, 0, 0, image.getWidth(), image.getHeight()); + } + + /** + * 构造 + * + * @param image {@link BufferedImage} + * @param left 左边间隔 + * @param top 顶部间隔 + * @param width 宽度 + * @param height 高度 + */ + public BufferedImageLuminanceSource(BufferedImage image, int left, int top, int width, int height) { + super(width, height); + + int sourceWidth = image.getWidth(); + int sourceHeight = image.getHeight(); + if (left + width > sourceWidth || top + height > sourceHeight) { + throw new IllegalArgumentException("Crop rectangle does not fit within image data."); + } + + for (int y = top; y < top + height; y++) { + for (int x = left; x < left + width; x++) { + if ((image.getRGB(x, y) & 0xFF000000) == 0) { + image.setRGB(x, y, 0xFFFFFFFF); // = white + } + } + } + + this.image = new BufferedImage(sourceWidth, sourceHeight, BufferedImage.TYPE_BYTE_GRAY); + this.image.getGraphics().drawImage(image, 0, 0, null); + this.left = left; + this.top = top; + } + + @Override + public byte[] getRow(int y, byte[] row) { + if (y < 0 || y >= getHeight()) { + throw new IllegalArgumentException("Requested row is outside the image: " + y); + } + int width = getWidth(); + if (row == null || row.length < width) { + row = new byte[width]; + } + image.getRaster().getDataElements(left, top + y, width, 1, row); + return row; + } + + @Override + public byte[] getMatrix() { + int width = getWidth(); + int height = getHeight(); + int area = width * height; + byte[] matrix = new byte[area]; + image.getRaster().getDataElements(left, top, width, height, matrix); + return matrix; + } + + @Override + public boolean isCropSupported() { + return true; + } + + @Override + public LuminanceSource crop(int left, int top, int width, int height) { + return new BufferedImageLuminanceSource(image, this.left + left, this.top + top, width, height); + } + + @Override + public boolean isRotateSupported() { + return true; + } + + @Override + public LuminanceSource rotateCounterClockwise() { + + int sourceWidth = image.getWidth(); + int sourceHeight = image.getHeight(); + + AffineTransform transform = new AffineTransform(0.0, -1.0, 1.0, 0.0, 0.0, sourceWidth); + + BufferedImage rotatedImage = new BufferedImage(sourceHeight, sourceWidth, BufferedImage.TYPE_BYTE_GRAY); + + Graphics2D g = rotatedImage.createGraphics(); + g.drawImage(image, transform, null); + g.dispose(); + + int width = getWidth(); + return new BufferedImageLuminanceSource(rotatedImage, top, sourceWidth - (left + width), getHeight(), width); + } + +} \ No newline at end of file diff --git a/hutool-extra/src/main/java/cn/hutool/extra/qrcode/QrCodeException.java b/hutool-extra/src/main/java/cn/hutool/extra/qrcode/QrCodeException.java new file mode 100644 index 000000000..5889092d9 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/qrcode/QrCodeException.java @@ -0,0 +1,33 @@ +package cn.hutool.extra.qrcode; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.StrUtil; + +/** + * Qrcode异常 + * + * @author xiaoleilu + */ +public class QrCodeException extends RuntimeException { + private static final long serialVersionUID = 8247610319171014183L; + + public QrCodeException(Throwable e) { + super(ExceptionUtil.getMessage(e), e); + } + + public QrCodeException(String message) { + super(message); + } + + public QrCodeException(String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params)); + } + + public QrCodeException(String message, Throwable throwable) { + super(message, throwable); + } + + public QrCodeException(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/qrcode/QrCodeUtil.java b/hutool-extra/src/main/java/cn/hutool/extra/qrcode/QrCodeUtil.java new file mode 100644 index 000000000..0b2219603 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/qrcode/QrCodeUtil.java @@ -0,0 +1,352 @@ +package cn.hutool.extra.qrcode; + +import java.awt.Image; +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashMap; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.Binarizer; +import com.google.zxing.BinaryBitmap; +import com.google.zxing.DecodeHintType; +import com.google.zxing.LuminanceSource; +import com.google.zxing.MultiFormatReader; +import com.google.zxing.MultiFormatWriter; +import com.google.zxing.NotFoundException; +import com.google.zxing.Result; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.common.HybridBinarizer; + +import cn.hutool.core.img.Img; +import cn.hutool.core.img.ImgUtil; +import cn.hutool.core.util.CharsetUtil; + +/** + * 基于Zxing的二维码工具类 + * + * @author looly + * @since 4.0.2 + * + */ +public class QrCodeUtil { + + /** + * 生成PNG格式的二维码图片,以byte[]形式表示 + * + * @param content 内容 + * @param width 宽度 + * @param height 高度 + * @return 图片的byte[] + * @since 4.0.10 + */ + public static byte[] generatePng(String content, int width, int height) { + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + generate(content, width, height, ImgUtil.IMAGE_TYPE_PNG, out); + return out.toByteArray(); + } + + /** + * 生成PNG格式的二维码图片,以byte[]形式表示 + * + * @param content 内容 + * @param config 二维码配置,包括长、宽、边距、颜色等 + * @return 图片的byte[] + * @since 4.1.2 + */ + public static byte[] generatePng(String content, QrConfig config) { + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + generate(content, config, ImgUtil.IMAGE_TYPE_PNG, out); + return out.toByteArray(); + } + + /** + * 生成二维码到文件,二维码图片格式取决于文件的扩展名 + * + * @param content 文本内容 + * @param width 宽度 + * @param height 高度 + * @param targetFile 目标文件,扩展名决定输出格式 + * @return 目标文件 + */ + public static File generate(String content, int width, int height, File targetFile) { + final BufferedImage image = generate(content, width, height); + ImgUtil.write(image, targetFile); + return targetFile; + } + + /** + * 生成二维码到文件,二维码图片格式取决于文件的扩展名 + * + * @param content 文本内容 + * @param config 二维码配置,包括长、宽、边距、颜色等 + * @param targetFile 目标文件,扩展名决定输出格式 + * @return 目标文件 + * @since 4.1.2 + */ + public static File generate(String content, QrConfig config, File targetFile) { + final BufferedImage image = generate(content, config); + ImgUtil.write(image, targetFile); + return targetFile; + } + + /** + * 生成二维码到输出流 + * + * @param content 文本内容 + * @param width 宽度 + * @param height 高度 + * @param imageType 图片类型(图片扩展名),见{@link ImgUtil} + * @param out 目标流 + */ + public static void generate(String content, int width, int height, String imageType, OutputStream out) { + final BufferedImage image = generate(content, width, height); + ImgUtil.write(image, imageType, out); + } + + /** + * 生成二维码到输出流 + * + * @param content 文本内容 + * @param config 二维码配置,包括长、宽、边距、颜色等 + * @param imageType 图片类型(图片扩展名),见{@link ImgUtil} + * @param out 目标流 + * @since 4.1.2 + */ + public static void generate(String content, QrConfig config, String imageType, OutputStream out) { + final BufferedImage image = generate(content, config); + ImgUtil.write(image, imageType, out); + } + + /** + * 生成二维码图片 + * + * @param content 文本内容 + * @param width 宽度 + * @param height 高度 + * @return 二维码图片(黑白) + */ + public static BufferedImage generate(String content, int width, int height) { + return generate(content, new QrConfig(width, height)); + } + + /** + * 生成二维码或条形码图片 + * + * @param content 文本内容 + * @param format 格式,可选二维码或者条形码 + * @param width 宽度 + * @param height 高度 + * @return 二维码图片(黑白) + */ + public static BufferedImage generate(String content, BarcodeFormat format, int width, int height) { + return generate(content, format, new QrConfig(width, height)); + } + + /** + * 生成二维码图片 + * + * @param content 文本内容 + * @param config 二维码配置,包括长、宽、边距、颜色等 + * @return 二维码图片(黑白) + * @since 4.1.2 + */ + public static BufferedImage generate(String content, QrConfig config) { + return generate(content, BarcodeFormat.QR_CODE, config); + } + + /** + * 生成二维码或条形码图片
+ * 只有二维码时QrConfig中的图片才有效 + * + * @param content 文本内容 + * @param format 格式,可选二维码、条形码等 + * @param config 二维码配置,包括长、宽、边距、颜色等 + * @return 二维码图片(黑白) + * @since 4.1.14 + */ + public static BufferedImage generate(String content, BarcodeFormat format, QrConfig config) { + final BitMatrix bitMatrix = encode(content, format, config); + final BufferedImage image = toImage(bitMatrix, config.foreColor, config.backColor); + final Image logoImg = config.img; + if (null != logoImg && BarcodeFormat.QR_CODE == format) { + // 只有二维码可以贴图 + final int qrWidth = image.getWidth(); + final int qrHeight = image.getHeight(); + int width; + int height; + // 按照最短的边做比例缩放 + if (qrWidth < qrHeight) { + width = qrWidth / config.ratio; + height = logoImg.getHeight(null) * width / logoImg.getWidth(null); + } else { + height = qrHeight / config.ratio; + width = logoImg.getWidth(null) * height / logoImg.getHeight(null); + } + + Img.from(image).pressImage(// + Img.from(logoImg).round(0.3).getImg(), // 圆角 + new Rectangle(width, height), // + 1// + ); + } + return image; + } + + // ------------------------------------------------------------------------------------------------------------------- encode + /** + * 将文本内容编码为二维码 + * + * @param content 文本内容 + * @param width 宽度 + * @param height 高度 + * @return {@link BitMatrix} + */ + public static BitMatrix encode(String content, int width, int height) { + return encode(content, BarcodeFormat.QR_CODE, width, height); + } + + /** + * 将文本内容编码为二维码 + * + * @param content 文本内容 + * @param config 二维码配置,包括长、宽、边距、颜色等 + * @return {@link BitMatrix} + * @since 4.1.2 + */ + public static BitMatrix encode(String content, QrConfig config) { + return encode(content, BarcodeFormat.QR_CODE, config); + } + + /** + * 将文本内容编码为条形码或二维码 + * + * @param content 文本内容 + * @param format 格式枚举 + * @param width 宽度 + * @param height 高度 + * @return {@link BitMatrix} + */ + public static BitMatrix encode(String content, BarcodeFormat format, int width, int height) { + return encode(content, format, new QrConfig(width, height)); + } + + /** + * 将文本内容编码为条形码或二维码 + * + * @param content 文本内容 + * @param format 格式枚举 + * @param config 二维码配置,包括长、宽、边距、颜色等 + * @return {@link BitMatrix} + * @since 4.1.2 + */ + public static BitMatrix encode(String content, BarcodeFormat format, QrConfig config) { + final MultiFormatWriter multiFormatWriter = new MultiFormatWriter(); + if (null == config) { + // 默认配置 + config = new QrConfig(); + } + BitMatrix bitMatrix; + try { + bitMatrix = multiFormatWriter.encode(content, format, config.width, config.height, config.toHints()); + } catch (WriterException e) { + throw new QrCodeException(e); + } + + return bitMatrix; + } + + // ------------------------------------------------------------------------------------------------------------------- decode + /** + * 解码二维码图片为文本 + * + * @param qrCodeInputstream 二维码输入流 + * @return 解码文本 + */ + public static String decode(InputStream qrCodeInputstream) { + return decode(ImgUtil.read(qrCodeInputstream)); + } + + /** + * 解码二维码图片为文本 + * + * @param qrCodeFile 二维码文件 + * @return 解码文本 + */ + public static String decode(File qrCodeFile) { + return decode(ImgUtil.read(qrCodeFile)); + } + + /** + * 将二维码图片解码为文本 + * + * @param image {@link Image} 二维码图片 + * @return 解码后的文本 + */ + public static String decode(Image image) { + return decode(image, true, false); + } + + /** + * 将二维码图片解码为文本 + * + * @param image {@link Image} 二维码图片 + * @param isTryHarder 是否优化精度 + * @param isPureBarcode 是否使用复杂模式,扫描带logo的二维码设为true + * @return 解码后的文本 + * @since 4.3.1 + */ + public static String decode(Image image, boolean isTryHarder, boolean isPureBarcode) { + final MultiFormatReader formatReader = new MultiFormatReader(); + + final LuminanceSource source = new BufferedImageLuminanceSource(ImgUtil.toBufferedImage(image)); + final Binarizer binarizer = new HybridBinarizer(source); + final BinaryBitmap binaryBitmap = new BinaryBitmap(binarizer); + + final HashMap hints = new HashMap<>(); + hints.put(DecodeHintType.CHARACTER_SET, CharsetUtil.UTF_8); + // 优化精度 + hints.put(DecodeHintType.TRY_HARDER, Boolean.valueOf(isTryHarder)); + // 复杂模式,开启PURE_BARCODE模式 + hints.put(DecodeHintType.PURE_BARCODE, Boolean.valueOf(isPureBarcode)); + Result result; + try { + result = formatReader.decode(binaryBitmap, hints); + } catch (NotFoundException e) { + // 报错尝试关闭复杂模式 + hints.remove(DecodeHintType.PURE_BARCODE); + try { + result = formatReader.decode(binaryBitmap, hints); + } catch (NotFoundException e1) { + throw new QrCodeException(e1); + } + } + + return result.getText(); + } + + /** + * BitMatrix转BufferedImage + * + * @param matrix BitMatrix + * @param foreColor 前景色 + * @param backColor 背景色 + * @return BufferedImage + * @since 4.1.2 + */ + public static BufferedImage toImage(BitMatrix matrix, int foreColor, int backColor) { + final int width = matrix.getWidth(); + final int height = matrix.getHeight(); + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + image.setRGB(x, y, matrix.get(x, y) ? foreColor : backColor); + } + } + return image; + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/qrcode/QrConfig.java b/hutool-extra/src/main/java/cn/hutool/extra/qrcode/QrConfig.java new file mode 100644 index 000000000..a49039bc4 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/qrcode/QrConfig.java @@ -0,0 +1,291 @@ +package cn.hutool.extra.qrcode; + +import java.awt.Image; +import java.io.File; +import java.nio.charset.Charset; +import java.util.HashMap; + +import com.google.zxing.EncodeHintType; +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; + +import cn.hutool.core.img.ImgUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.CharsetUtil; + +/** + * 二维码设置 + * + * @author looly + * @since 4.1.2 + */ +public class QrConfig { + + private static final int BLACK = 0xFF000000; + private static final int WHITE = 0xFFFFFFFF; + + /** 宽 */ + protected int width; + /** 长 */ + protected int height; + /** 前景色(二维码颜色) */ + protected int foreColor = BLACK; + /** 背景色 */ + protected int backColor = WHITE; + /** 边距1~4 */ + protected Integer margin = 2; + /** 纠错级别 */ + protected ErrorCorrectionLevel errorCorrection = ErrorCorrectionLevel.M; + /** 编码 */ + protected Charset charset = CharsetUtil.CHARSET_UTF_8; + /** 二维码中的Logo */ + protected Image img; + /** 二维码中的Logo缩放的比例系数,如5表示长宽最小值的1/5 */ + protected int ratio = 6; + + /** + * 创建QrConfig + * @return QrConfig + * @since 4.1.14 + */ + public static QrConfig create() { + return new QrConfig(); + } + + /** + * 构造,默认长宽为300 + */ + public QrConfig() { + this(300, 300); + } + + /** + * 构造 + * + * @param width 宽 + * @param height 长 + */ + public QrConfig(int width, int height) { + this.width = width; + this.height = height; + } + + /** + * 获取宽度 + * + * @return 宽度 + */ + public int getWidth() { + return width; + } + + /** + * 设置宽度 + * + * @param width 宽度 + * @return this + */ + public QrConfig setWidth(int width) { + this.width = width; + return this; + } + + /** + * 获取高度 + * + * @return 高度 + */ + public int getHeight() { + return height; + } + + /** + * 设置高度 + * + * @param height 高度 + * @return this; + */ + public QrConfig setHeight(int height) { + this.height = height; + return this; + } + + /** + * 获取前景色 + * + * @return 前景色 + */ + public int getForeColor() { + return foreColor; + } + + /** + * 设置前景色,例如:Color.BLUE.getRGB() + * + * @param foreColor 前景色 + * @return this + */ + public QrConfig setForeColor(int foreColor) { + this.foreColor = foreColor; + return this; + } + + /** + * 获取背景色 + * + * @return 背景色 + */ + public int getBackColor() { + return backColor; + } + + /** + * 设置背景色,例如:Color.BLUE.getRGB() + * + * @param backColor 背景色 + * @return this + */ + public QrConfig setBackColor(int backColor) { + this.backColor = backColor; + return this; + } + + /** + * 获取边距 + * + * @return 边距 + */ + public Integer getMargin() { + return margin; + } + + /** + * 设置边距 + * + * @param margin 边距 + * @return this + */ + public QrConfig setMargin(Integer margin) { + this.margin = margin; + return this; + } + + /** + * 获取纠错级别 + * + * @return 纠错级别 + */ + public ErrorCorrectionLevel getErrorCorrection() { + return errorCorrection; + } + + /** + * 设置纠错级别 + * + * @param errorCorrection 纠错级别 + * @return this + */ + public QrConfig setErrorCorrection(ErrorCorrectionLevel errorCorrection) { + this.errorCorrection = errorCorrection; + return this; + } + + /** + * 获取编码 + * + * @return 编码 + */ + public Charset getCharset() { + return charset; + } + + /** + * 设置编码 + * + * @param charset 编码 + * @return this + */ + public QrConfig setCharset(Charset charset) { + this.charset = charset; + return this; + } + + /** + * 获取二维码中的Logo + * + * @return Logo图片 + */ + public Image getImg() { + return img; + } + + /** + * 设置二维码中的Logo文件 + * + * @param imgPath 二维码中的Logo路径 + * @return this; + */ + public QrConfig setImg(String imgPath) { + return setImg(FileUtil.file(imgPath)); + } + + /** + * 设置二维码中的Logo文件 + * + * @param imgFile 二维码中的Logo + * @return this; + */ + public QrConfig setImg(File imgFile) { + return setImg(ImgUtil.read(imgFile)); + } + + /** + * 设置二维码中的Logo + * + * @param img 二维码中的Logo + * @return this; + */ + public QrConfig setImg(Image img) { + this.img = img; + return this; + } + + /** + * 获取二维码中的Logo缩放的比例系数,如5表示长宽最小值的1/5 + * + * @return 二维码中的Logo缩放的比例系数,如5表示长宽最小值的1/5 + */ + public int getRatio() { + return this.ratio; + } + + /** + * 设置二维码中的Logo缩放的比例系数,如5表示长宽最小值的1/5 + * + * @param ratio 二维码中的Logo缩放的比例系数,如5表示长宽最小值的1/5 + * @return this; + */ + public QrConfig setRatio(int ratio) { + this.ratio = ratio; + return this; + } + + /** + * 转换为Zxing的二维码配置 + * + * @return 配置 + */ + public HashMap toHints() { + // 配置 + final HashMap hints = new HashMap<>(); + if (null != this.charset) { + hints.put(EncodeHintType.CHARACTER_SET, charset.toString()); + } + if (null != this.errorCorrection) { + hints.put(EncodeHintType.ERROR_CORRECTION, this.errorCorrection); + } + if (null != this.margin) { + hints.put(EncodeHintType.MARGIN, this.margin); + } + return hints; + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/qrcode/package-info.java b/hutool-extra/src/main/java/cn/hutool/extra/qrcode/package-info.java new file mode 100644 index 000000000..75c2592e0 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/qrcode/package-info.java @@ -0,0 +1,7 @@ +/** + * 二维码封装,基于zxing库,入口为QrCodeUtil + * + * @author looly + * + */ +package cn.hutool.extra.qrcode; \ No newline at end of file diff --git a/hutool-extra/src/main/java/cn/hutool/extra/servlet/ServletUtil.java b/hutool-extra/src/main/java/cn/hutool/extra/servlet/ServletUtil.java new file mode 100644 index 000000000..98ee5f60a --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/servlet/ServletUtil.java @@ -0,0 +1,623 @@ +package cn.hutool.extra.servlet; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.ServletOutputStream; +import javax.servlet.ServletRequest; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.copier.CopyOptions; +import cn.hutool.core.bean.copier.ValueProvider; +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.util.ArrayUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; +import cn.hutool.extra.servlet.multipart.MultipartFormData; +import cn.hutool.extra.servlet.multipart.UploadSetting; + +/** + * Servlet相关工具类封装 + * + * @author looly + * @since 3.2.0 + */ +public class ServletUtil { + + public static final String METHOD_DELETE = "DELETE"; + public static final String METHOD_HEAD = "HEAD"; + public static final String METHOD_GET = "GET"; + public static final String METHOD_OPTIONS = "OPTIONS"; + public static final String METHOD_POST = "POST"; + public static final String METHOD_PUT = "PUT"; + public static final String METHOD_TRACE = "TRACE"; + + // --------------------------------------------------------- getParam start + /** + * 获得所有请求参数 + * + * @param request 请求对象{@link ServletRequest} + * @return Map + */ + public static Map getParams(ServletRequest request) { + final Map map = request.getParameterMap(); + return Collections.unmodifiableMap(map); + } + + /** + * 获得所有请求参数 + * + * @param request 请求对象{@link ServletRequest} + * @return Map + */ + public static Map getParamMap(ServletRequest request) { + Map params = new HashMap(); + for (Map.Entry entry : getParams(request).entrySet()) { + params.put(entry.getKey(), ArrayUtil.join(entry.getValue(), StrUtil.COMMA)); + } + return params; + } + + /** + * 获取请求体
+ * 调用该方法后,getParam方法将失效 + * + * @param request {@link ServletRequest} + * @return 获得请求体 + * @since 4.0.2 + */ + public static String getBody(ServletRequest request) { + try { + return IoUtil.read(request.getReader()); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 获取请求体byte[]
+ * 调用该方法后,getParam方法将失效 + * + * @param request {@link ServletRequest} + * @return 获得请求体byte[] + * @since 4.0.2 + */ + public static byte[] getBodyBytes(ServletRequest request) { + try { + return IoUtil.readBytes(request.getInputStream()); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + // --------------------------------------------------------- getParam end + + // --------------------------------------------------------- fillBean start + /** + * ServletRequest 参数转Bean + * + * @param Bean类型 + * @param request ServletRequest + * @param bean Bean + * @param copyOptions 注入时的设置 + * @return Bean + * @since 3.0.4 + */ + public static T fillBean(final ServletRequest request, T bean, CopyOptions copyOptions) { + final String beanName = StrUtil.lowerFirst(bean.getClass().getSimpleName()); + return BeanUtil.fillBean(bean, new ValueProvider() { + @Override + public Object value(String key, Type valueType) { + String value = request.getParameter(key); + if (StrUtil.isEmpty(value)) { + // 使用类名前缀尝试查找值 + value = request.getParameter(beanName + StrUtil.DOT + key); + if (StrUtil.isEmpty(value)) { + // 此处取得的值为空时跳过,包括null和"" + value = null; + } + } + return value; + } + + @Override + public boolean containsKey(String key) { + // 对于Servlet来说,返回值null意味着无此参数 + return (null != request.getParameter(key)) || (null != request.getParameter(beanName + StrUtil.DOT + key)); + } + }, copyOptions); + } + + /** + * ServletRequest 参数转Bean + * + * @param Bean类型 + * @param request {@link ServletRequest} + * @param bean Bean + * @param isIgnoreError 是否忽略注入错误 + * @return Bean + */ + public static T fillBean(ServletRequest request, T bean, boolean isIgnoreError) { + return fillBean(request, bean, CopyOptions.create().setIgnoreError(isIgnoreError)); + } + + /** + * ServletRequest 参数转Bean + * + * @param Bean类型 + * @param request ServletRequest + * @param beanClass Bean Class + * @param isIgnoreError 是否忽略注入错误 + * @return Bean + */ + public static T toBean(ServletRequest request, Class beanClass, boolean isIgnoreError) { + return fillBean(request, ReflectUtil.newInstance(beanClass), isIgnoreError); + } + // --------------------------------------------------------- fillBean end + + /** + * 获取客户端IP + * + *

+ * 默认检测的Header: + * + *

+	 * 1、X-Forwarded-For
+	 * 2、X-Real-IP
+	 * 3、Proxy-Client-IP
+	 * 4、WL-Proxy-Client-IP
+	 * 
+ *

+ * + *

+ * otherHeaderNames参数用于自定义检测的Header
+ * 需要注意的是,使用此方法获取的客户IP地址必须在Http服务器(例如Nginx)中配置头信息,否则容易造成IP伪造。 + *

+ * + * @param request 请求对象{@link HttpServletRequest} + * @param otherHeaderNames 其他自定义头文件,通常在Http服务器(例如Nginx)中配置 + * @return IP地址 + */ + public static String getClientIP(HttpServletRequest request, String... otherHeaderNames) { + String[] headers = { "X-Forwarded-For", "X-Real-IP", "Proxy-Client-IP", "WL-Proxy-Client-IP", "HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR" }; + if (ArrayUtil.isNotEmpty(otherHeaderNames)) { + headers = ArrayUtil.addAll(headers, otherHeaderNames); + } + + return getClientIPByHeader(request, headers); + } + + /** + * 获取客户端IP + * + *

+ * headerNames参数用于自定义检测的Header
+ * 需要注意的是,使用此方法获取的客户IP地址必须在Http服务器(例如Nginx)中配置头信息,否则容易造成IP伪造。 + *

+ * + * @param request 请求对象{@link HttpServletRequest} + * @param headerNames 自定义头,通常在Http服务器(例如Nginx)中配置 + * @return IP地址 + * @since 4.4.1 + */ + public static String getClientIPByHeader(HttpServletRequest request, String... headerNames) { + String ip; + for (String header : headerNames) { + ip = request.getHeader(header); + if (false == isUnknow(ip)) { + return getMultistageReverseProxyIp(ip); + } + } + + ip = request.getRemoteAddr(); + return getMultistageReverseProxyIp(ip); + } + + /** + * 获得MultiPart表单内容,多用于获得上传的文件 在同一次请求中,此方法只能被执行一次! + * + * @param request {@link ServletRequest} + * @return MultipartFormData + * @throws IORuntimeException IO异常 + * @since 4.0.2 + */ + public static MultipartFormData getMultipart(ServletRequest request) throws IORuntimeException { + return getMultipart(request, new UploadSetting()); + } + + /** + * 获得multipart/form-data 表单内容
+ * 包括文件和普通表单数据
+ * 在同一次请求中,此方法只能被执行一次! + * + * @param request {@link ServletRequest} + * @param uploadSetting 上传文件的设定,包括最大文件大小、保存在内存的边界大小、临时目录、扩展名限定等 + * @return MultiPart表单 + * @throws IORuntimeException IO异常 + * @since 4.0.2 + */ + public static MultipartFormData getMultipart(ServletRequest request, UploadSetting uploadSetting) throws IORuntimeException { + final MultipartFormData formData = new MultipartFormData(uploadSetting); + try { + formData.parseRequest(request); + } catch (IOException e) { + throw new IORuntimeException(e); + } + + return formData; + } + + // --------------------------------------------------------- Header start + /** + * 忽略大小写获得请求header中的信息 + * + * @param request 请求对象{@link HttpServletRequest} + * @param nameIgnoreCase 忽略大小写头信息的KEY + * @return header值 + */ + public final static String getHeaderIgnoreCase(HttpServletRequest request, String nameIgnoreCase) { + Enumeration names = request.getHeaderNames(); + String name = null; + while (names.hasMoreElements()) { + name = names.nextElement(); + if (name != null && name.equalsIgnoreCase(nameIgnoreCase)) { + return request.getHeader(name); + } + } + + return null; + } + + /** + * 获得请求header中的信息 + * + * @param request 请求对象{@link HttpServletRequest} + * @param name 头信息的KEY + * @param charset 字符集 + * @return header值 + */ + public final static String getHeader(HttpServletRequest request, String name, String charset) { + final String header = request.getHeader(name); + if (null != header) { + try { + return new String(header.getBytes(CharsetUtil.ISO_8859_1), charset); + } catch (UnsupportedEncodingException e) { + throw new UtilException(StrUtil.format("Error charset {} for http request header.", charset)); + } + } + return null; + } + + /** + * 客户浏览器是否为IE + * + * @param request 请求对象{@link HttpServletRequest} + * @return 客户浏览器是否为IE + */ + public static boolean isIE(HttpServletRequest request) { + String userAgent = getHeaderIgnoreCase(request, "User-Agent"); + if (StrUtil.isNotBlank(userAgent)) { + userAgent = userAgent.toUpperCase(); + if (userAgent.contains("MSIE") || userAgent.contains("TRIDENT")) { + return true; + } + } + return false; + } + + /** + * 是否为GET请求 + * + * @param request 请求对象{@link HttpServletRequest} + * @return 是否为GET请求 + */ + public static boolean isGetMethod(HttpServletRequest request) { + return METHOD_GET.equalsIgnoreCase(request.getMethod()); + } + + /** + * 是否为POST请求 + * + * @param request 请求对象{@link HttpServletRequest} + * @return 是否为POST请求 + */ + public static boolean isPostMethod(HttpServletRequest request) { + return METHOD_POST.equalsIgnoreCase(request.getMethod()); + } + + /** + * 是否为Multipart类型表单,此类型表单用于文件上传 + * + * @param request 请求对象{@link HttpServletRequest} + * @return 是否为Multipart类型表单,此类型表单用于文件上传 + */ + public static boolean isMultipart(HttpServletRequest request) { + if (false == isPostMethod(request)) { + return false; + } + + String contentType = request.getContentType(); + if (StrUtil.isBlank(contentType)) { + return false; + } + if (contentType.toLowerCase().startsWith("multipart/")) { + return true; + } + + return false; + } + // --------------------------------------------------------- Header end + + // --------------------------------------------------------- Cookie start + /** + * 获得指定的Cookie + * + * @param httpServletRequest {@link HttpServletRequest} + * @param name cookie名 + * @return Cookie对象 + */ + public final static Cookie getCookie(HttpServletRequest httpServletRequest, String name) { + final Map cookieMap = readCookieMap(httpServletRequest); + return cookieMap == null ? null : cookieMap.get(name); + } + + /** + * 将cookie封装到Map里面 + * + * @param httpServletRequest {@link HttpServletRequest} + * @return Cookie map + */ + public final static Map readCookieMap(HttpServletRequest httpServletRequest) { + Map cookieMap = new HashMap(); + Cookie[] cookies = httpServletRequest.getCookies(); + if (null == cookies) { + return null; + } + for (Cookie cookie : cookies) { + cookieMap.put(cookie.getName().toLowerCase(), cookie); + } + return cookieMap; + } + + /** + * 设定返回给客户端的Cookie + * + * @param response 响应对象{@link HttpServletResponse} + * @param cookie Servlet Cookie对象 + */ + public final static void addCookie(HttpServletResponse response, Cookie cookie) { + response.addCookie(cookie); + } + + /** + * 设定返回给客户端的Cookie + * + * @param response 响应对象{@link HttpServletResponse} + * @param name Cookie名 + * @param value Cookie值 + */ + public final static void addCookie(HttpServletResponse response, String name, String value) { + response.addCookie(new Cookie(name, value)); + } + + /** + * 设定返回给客户端的Cookie + * + * @param response 响应对象{@link HttpServletResponse} + * @param name cookie名 + * @param value cookie值 + * @param maxAgeInSeconds -1: 关闭浏览器清除Cookie. 0: 立即清除Cookie. >0 : Cookie存在的秒数. + * @param path Cookie的有效路径 + * @param domain the domain name within which this cookie is visible; form is according to RFC 2109 + */ + public final static void addCookie(HttpServletResponse response, String name, String value, int maxAgeInSeconds, String path, String domain) { + Cookie cookie = new Cookie(name, value); + if (domain != null) { + cookie.setDomain(domain); + } + cookie.setMaxAge(maxAgeInSeconds); + cookie.setPath(path); + addCookie(response, cookie); + } + + /** + * 设定返回给客户端的Cookie
+ * Path: "/"
+ * No Domain + * + * @param response 响应对象{@link HttpServletResponse} + * @param name cookie名 + * @param value cookie值 + * @param maxAgeInSeconds -1: 关闭浏览器清除Cookie. 0: 立即清除Cookie. >0 : Cookie存在的秒数. + */ + public final static void addCookie(HttpServletResponse response, String name, String value, int maxAgeInSeconds) { + addCookie(response, name, value, maxAgeInSeconds, "/", null); + } + + // --------------------------------------------------------- Cookie end + // --------------------------------------------------------- Response start + /** + * 获得PrintWriter + * + * @param response 响应对象{@link HttpServletResponse} + * @return 获得PrintWriter + * @throws IORuntimeException IO异常 + */ + public static PrintWriter getWriter(HttpServletResponse response) throws IORuntimeException { + try { + return response.getWriter(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 返回数据给客户端 + * + * @param response 响应对象{@link HttpServletResponse} + * @param text 返回的内容 + * @param contentType 返回的类型 + */ + public static void write(HttpServletResponse response, String text, String contentType) { + response.setContentType(contentType); + Writer writer = null; + try { + writer = response.getWriter(); + writer.write(text); + writer.flush(); + } catch (IOException e) { + throw new UtilException(e); + } finally { + IoUtil.close(writer); + } + } + + /** + * 返回文件给客户端 + * + * @param response 响应对象{@link HttpServletResponse} + * @param file 写出的文件对象 + * @since 4.1.15 + */ + public static void write(HttpServletResponse response, File file) { + final String fileName = file.getName(); + final String contentType = ObjectUtil.defaultIfNull(FileUtil.getMimeType(fileName), "application/octet-stream"); + BufferedInputStream in = null; + try { + in = FileUtil.getInputStream(file); + write(response, in, contentType, fileName); + } finally { + IoUtil.close(in); + } + } + + /** + * 返回数据给客户端 + * + * @param response 响应对象{@link HttpServletResponse} + * @param in 需要返回客户端的内容 + * @param contentType 返回的类型 + * @param fileName 文件名 + * @since 4.1.15 + */ + public static void write(HttpServletResponse response, InputStream in, String contentType, String fileName) { + final String charset = ObjectUtil.defaultIfNull(response.getCharacterEncoding(), CharsetUtil.UTF_8); + response.setHeader("Content-Disposition", StrUtil.format("attachment;filename={}", URLUtil.encode(fileName, charset))); + response.setContentType(contentType); + write(response, in); + } + + /** + * 返回数据给客户端 + * + * @param response 响应对象{@link HttpServletResponse} + * @param in 需要返回客户端的内容 + * @param contentType 返回的类型 + */ + public static void write(HttpServletResponse response, InputStream in, String contentType) { + response.setContentType(contentType); + write(response, in); + } + + /** + * 返回数据给客户端 + * + * @param response 响应对象{@link HttpServletResponse} + * @param in 需要返回客户端的内容 + */ + public static void write(HttpServletResponse response, InputStream in) { + write(response, in, IoUtil.DEFAULT_BUFFER_SIZE); + } + + /** + * 返回数据给客户端 + * + * @param response 响应对象{@link HttpServletResponse} + * @param in 需要返回客户端的内容 + * @param bufferSize 缓存大小 + */ + public static void write(HttpServletResponse response, InputStream in, int bufferSize) { + ServletOutputStream out = null; + try { + out = response.getOutputStream(); + IoUtil.copy(in, out, bufferSize); + } catch (IOException e) { + throw new UtilException(e); + } finally { + IoUtil.close(out); + IoUtil.close(in); + } + } + + /** + * 设置响应的Header + * + * @param response 响应对象{@link HttpServletResponse} + * @param name 名 + * @param value 值,可以是String,Date, int + */ + public static void setHeader(HttpServletResponse response, String name, Object value) { + if (value instanceof String) { + response.setHeader(name, (String) value); + } else if (Date.class.isAssignableFrom(value.getClass())) { + response.setDateHeader(name, ((Date) value).getTime()); + } else if (value instanceof Integer || "int".equals(value.getClass().getSimpleName().toLowerCase())) { + response.setIntHeader(name, (Integer) value); + } else { + response.setHeader(name, value.toString()); + } + } + // --------------------------------------------------------- Response end + + // --------------------------------------------------------- Private methd start + /** + * 从多级反向代理中获得第一个非unknown IP地址 + * + * @param ip 获得的IP地址 + * @return 第一个非unknown IP地址 + */ + private static String getMultistageReverseProxyIp(String ip) { + // 多级反向代理检测 + if (ip != null && ip.indexOf(",") > 0) { + final String[] ips = ip.trim().split(","); + for (String subIp : ips) { + if (false == isUnknow(subIp)) { + ip = subIp; + break; + } + } + } + return ip; + } + + /** + * 检测给定字符串是否为未知,多用于检测HTTP请求相关
+ * + * @param checkString 被检测的字符串 + * @return 是否未知 + */ + private static boolean isUnknow(String checkString) { + return StrUtil.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString); + } + // --------------------------------------------------------- Private methd end + +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/servlet/multipart/MultipartFormData.java b/hutool-extra/src/main/java/cn/hutool/extra/servlet/multipart/MultipartFormData.java new file mode 100644 index 000000000..a7e32fe57 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/servlet/multipart/MultipartFormData.java @@ -0,0 +1,257 @@ +package cn.hutool.extra.servlet.multipart; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import javax.servlet.ServletRequest; + +import cn.hutool.core.util.ArrayUtil; + +/** + * HttpRequest解析器
+ * 来自Jodd + * + * @author jodd.org + */ +public class MultipartFormData { + + /** 请求参数 */ + private Map requestParameters = new HashMap(); + /** 请求文件 */ + private Map requestFiles = new HashMap(); + + /** 是否解析完毕 */ + private boolean loaded; + /** 上传选项 */ + private UploadSetting setting; + + // --------------------------------------------------------------------- Constructor start + /** + * 构造 + */ + public MultipartFormData() { + this(null); + } + + /** + * 构造 + * + * @param uploadSetting 上传设定 + */ + public MultipartFormData(UploadSetting uploadSetting) { + this.setting = uploadSetting == null ? new UploadSetting() : uploadSetting; + } + // --------------------------------------------------------------------- Constructor end + + /** + * 解析上传文件和表单数据 + * + * @param request Http请求 + * @throws IOException IO异常 + */ + public void parseRequest(ServletRequest request) throws IOException { + parseRequestStream(request.getInputStream(), request.getCharacterEncoding()); + } + + /** + * 提取上传的文件和表单数据 + * + * @param inputStream HttpRequest流 + * @param charset 编码 + * @throws IOException IO异常 + */ + public void parseRequestStream(InputStream inputStream, String 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 == true) { + // 文件类型的表单项 + String fileName = header.fileName; + if (fileName.length() > 0 && header.contentType.contains("application/x-macbinary")) { + input.skipBytes(128); + } + UploadFile newFile = new UploadFile(header, setting); + newFile.processStream(input); + + putFile(header.formFieldName, newFile); + } else { + // 标准表单项 + ByteArrayOutputStream fbos = new ByteArrayOutputStream(1024); + input.copy(fbos); + String value = (charset != null) ? new String(fbos.toByteArray(), charset) : new String(fbos.toByteArray()); + putParameter(header.formFieldName, value); + } + + 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) { + if (requestParameters == null) { + return null; + } + String[] values = requestParameters.get(paramName); + if (ArrayUtil.isNotEmpty(values)) { + return values[0]; + } + return null; + } + + /** + * @return 获得参数名集合 + */ + public Set getParamNames() { + if (requestParameters == null) { + return Collections.emptySet(); + } + return requestParameters.keySet(); + } + + /** + * 获得数组表单值 + * + * @param paramName 参数名 + * @return 数组表单值 + */ + public String[] getArrayParam(String paramName) { + if (requestParameters == null) { + return null; + } + return requestParameters.get(paramName); + } + + /** + * 获取所有属性的集合 + * + * @return 所有属性的集合 + */ + public Map getParamMap() { + return 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) { + if (requestFiles == null) { + return null; + } + return requestFiles.get(paramName); + } + + /** + * 获取上传的文件属性名集合 + * + * @return 上传的文件属性名集合 + */ + public Set getFileParamNames() { + if (requestFiles == null) { + return Collections.emptySet(); + } + return requestFiles.keySet(); + } + + /** + * 获取文件映射 + * + * @return 文件映射 + */ + public Map getFileMap() { + 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) { + UploadFile[] fileUploads = requestFiles.get(name); + fileUploads = fileUploads == null ? new UploadFile[] { uploadFile } : ArrayUtil.append(fileUploads, uploadFile); + requestFiles.put(name, fileUploads); + } + + /** + * 加入普通参数 + * + * @param name 参数名 + * @param value 参数值 + */ + private void putParameter(String name, String value) { + String[] params = requestParameters.get(name); + params = params == null ? new String[] { value } : ArrayUtil.append(params, value); + requestParameters.put(name, params); + } + + /** + * 设置使输入流为解析状态,如果已解析,则抛出异常 + * + * @throws IOException IO异常 + */ + private void setLoaded() throws IOException { + if (loaded == true) { + throw new IOException("Multi-part request already parsed."); + } + loaded = true; + } + // ---------------------------------------------------------------- Private method end +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/servlet/multipart/MultipartRequestInputStream.java b/hutool-extra/src/main/java/cn/hutool/extra/servlet/multipart/MultipartRequestInputStream.java new file mode 100644 index 000000000..f5b8152d0 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/servlet/multipart/MultipartRequestInputStream.java @@ -0,0 +1,214 @@ +package cn.hutool.extra.servlet.multipart; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * 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数 + */ + public void skipBytes(int 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 + 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(String encoding) throws IOException { + String dataHeader = readDataHeaderString(encoding); + if (dataHeader != null) { + lastHeader = new UploadFileHeader(dataHeader); + } else { + lastHeader = null; + } + return lastHeader; + } + + protected String readDataHeaderString(String encoding) 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 encoding == null ? data.toString() : data.toString(encoding); + } + // ---------------------------------------------------------------- copy + + /** + * 全部字节流复制到out + * + * @param out 输出流 + * @return 复制的字节数 + * @throws IOException + */ + public int copy(OutputStream out) throws IOException { + int 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 int copy(OutputStream out, int limit) throws IOException { + int count = 0; + while (true) { + byte b = readByte(); + if (isBoundary(b)) { + break; + } + out.write(b); + count++; + if (count > limit) { + break; + } + } + return count; + } + + /** + * 跳过边界表示 + * + * @return 跳过的字节数 + */ + public int skipToBoundary() throws IOException { + int 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/hutool-extra/src/main/java/cn/hutool/extra/servlet/multipart/UploadFile.java b/hutool-extra/src/main/java/cn/hutool/extra/servlet/multipart/UploadFile.java new file mode 100644 index 000000000..8862a1fb7 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/servlet/multipart/UploadFile.java @@ -0,0 +1,271 @@ +package cn.hutool.extra.servlet.multipart; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; + +/** + * 上传的文件对象 + * + * @author xiaoleilu + * + */ +public class UploadFile { + private static Log log = LogFactory.get(); + + private static final String TMP_FILE_PREFIX = "hutool-"; + private static final String TMP_FILE_SUFFIX = ".upload.tmp"; + + private UploadFileHeader header; + private UploadSetting setting; + + private int 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) { + tempFile.delete(); + } + if (data != null) { + data = null; + } + } + + /** + * 将上传的文件写入指定的目标文件路径,自动创建文件
+ * 写入后原临时文件会被删除 + * @param destPath 目标文件路径 + * @return 目标文件 + * @throws IOException + */ + public File write(String destPath) throws IOException { + if(data != null || tempFile != null) { + return write(FileUtil.touch(destPath)); + } + return null; + } + + /** + * 将上传的文件写入目标文件
+ * 写入后原临时文件会被删除 + * + * @return 目标文件 + */ + public File write(File destination) throws IOException { + assertValid(); + + if (destination.isDirectory() == true) { + destination = new File(destination, this.header.getFileName()); + } + if (data != null) { + FileUtil.writeBytes(data, destination); + data = null; + } else { + if (tempFile != null) { + FileUtil.move(tempFile, destination, true); + } + } + return destination; + } + + /** + * Returns the content of file upload item. + */ + /** + * @return 获得文件字节流 + * @throws IOException + */ + public byte[] getFileContent() throws IOException { + assertValid(); + + if (data != null) { + return data; + } + if (tempFile != null) { + return FileUtil.readBytes(tempFile); + } + return null; + } + + /** + * @return 获得文件流 + * @throws IOException + */ + public InputStream getFileInputStream() throws IOException { + assertValid(); + + if (data != null) { + return new BufferedInputStream(new ByteArrayInputStream(data)); + } + if (tempFile != null) { + return new BufferedInputStream(new FileInputStream(tempFile)); + } + return null; + } + + // ---------------------------------------------------------------- header + + /** + * @return 上传文件头部信息 + */ + public UploadFileHeader getHeader() { + return header; + } + + /** + * @return 文件名 + */ + public String getFileName() { + return header == null ? null : header.getFileName(); + } + + // ---------------------------------------------------------------- properties + + /** + * @return 上传文件的大小,< 0 表示未上传 + */ + public int size() { + return size; + } + + /** + * @return 是否上传成功 + */ + public boolean isUploaded() { + return size > 0; + } + + /** + * @return 文件是否在内存中 + */ + public boolean isInMemory() { + return data != null; + } + + // ---------------------------------------------------------------- process + /** + * 处理上传表单流,提取出文件 + * + * @param input 上传表单的流 + * @throws IOException + */ + protected boolean processStream(MultipartRequestInputStream input) throws IOException { + if (!isAllowedExtension()) { + // 非允许的扩展名 + log.debug("Forbidden uploaded file [{}]", this.getFileName()); + size = input.skipToBoundary(); + return false; + } + size = 0; + + // 处理内存文件 + int memoryThreshold = setting.memoryThreshold; + if (memoryThreshold > 0) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(memoryThreshold); + int 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); + BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(tempFile)); + if (data != null) { + size = data.length; + out.write(data); + data = null; // not needed anymore + } + int 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) { + // 超出上传大小限制 + tempFile.delete(); + tempFile = null; + log.debug("Upload file [{}] too big, file size > [{}]", this.getFileName(), maxFileSize); + input.skipToBoundary(); + return false; + } + } finally { + IoUtil.close(out); + } + // if (getFileName().length() == 0 && size == 0) { + // size = -1; + // } + return true; + } + + // ---------------------------------------------------------------------------- Private method start + /** + * @return 是否为允许的扩展名 + */ + private boolean isAllowedExtension() { + String[] exts = setting.fileExts; + boolean isAllow = setting.isAllowFileExts; + if (exts == null || exts.length == 0) { + // 如果给定扩展名列表为空,当允许扩展名时全部允许,否则全部禁止 + return isAllow; + } + + String fileNameExt = FileUtil.extName(this.getFileName()); + for (String fileExtension : setting.fileExts) { + if (fileNameExt.equalsIgnoreCase(fileExtension)) { + return isAllow; + } + } + + // 未匹配到扩展名,如果为允许列表,返回false, 否则true + return !isAllow; + } + + /** + * 断言是否文件流可用 + * @throws IOException + */ + private void assertValid() throws IOException { + if(! isUploaded()) { + throw new IOException(StrUtil.format("File [{}] upload fail", getFileName())); + } + } + // ---------------------------------------------------------------------------- Private method end +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/servlet/multipart/UploadFileHeader.java b/hutool-extra/src/main/java/cn/hutool/extra/servlet/multipart/UploadFileHeader.java new file mode 100644 index 000000000..80468bee0 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/servlet/multipart/UploadFileHeader.java @@ -0,0 +1,187 @@ +package cn.hutool.extra.servlet.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 true if uploaded data are correctly marked as a file. This is true if header contains string 'filename'. + */ + public boolean isFile() { + return isFile; + } + + /** + * Returns form field name. + */ + public String getFormFieldName() { + return formFieldName; + } + + /** + * Returns complete file name as specified at client side. + */ + public String getFormFileName() { + return formFileName; + } + + /** + * Returns file name (base name and extension, without full path data). + */ + public String getFileName() { + return fileName; + } + + /** + * Returns uploaded content type. It is usually in the following form:
+ * mime_type/mime_subtype. + * + * @see #getMimeType() + * @see #getMimeSubtype() + */ + public String getContentType() { + return contentType; + } + + /** + * Returns file types MIME. + */ + public String getMimeType() { + return mimeType; + } + + /** + * Returns file sub type MIME. + */ + public String getMimeSubtype() { + return mimeSubtype; + } + + /** + * Returns content disposition. Usually it is 'form-data'. + */ + 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/hutool-extra/src/main/java/cn/hutool/extra/servlet/multipart/UploadSetting.java b/hutool-extra/src/main/java/cn/hutool/extra/servlet/multipart/UploadSetting.java new file mode 100644 index 000000000..35e4f9041 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/servlet/multipart/UploadSetting.java @@ -0,0 +1,149 @@ +package cn.hutool.extra.servlet.multipart; + +import java.net.URL; + +import cn.hutool.core.util.URLUtil; +import cn.hutool.log.Log; +import cn.hutool.log.StaticLog; +import cn.hutool.setting.Setting; + +/** + * 上传文件设定文件 + * + * @author xiaoleilu + * + */ +public class UploadSetting { + private static Log log = StaticLog.get(); + + /** 默认的配置文件路径(相对ClassPath) */ + public final static String DEFAULT_SETTING_PATH = "config/upload.setting"; + + /** 最大文件大小,默认无限制 */ + protected int 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 int getMaxFileSize() { + return maxFileSize; + } + + /** + * 设定最大文件大小,-1表示无限制 + * + * @param maxFileSize 最大文件大小 + */ + public void setMaxFileSize(int 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 + + /** + * 加载全局设定
+ * 使用默认的配置文件classpath/config/upload.setting + */ + public void load() { + load(DEFAULT_SETTING_PATH); + } + + /** + * 加载全局设定 + * + * @param settingPath 设定文件路径,相对Classpath + */ + synchronized public void load(String settingPath) { + URL url = URLUtil.getURL(settingPath); + if (url == null) { + log.info("Can not find Upload setting file [{}], use default setting.", settingPath); + return; + } + Setting setting = new Setting(url, Setting.DEFAULT_CHARSET, true); + + maxFileSize = setting.getInt("file.size.max"); + memoryThreshold = setting.getInt("memory.threshold"); + tmpUploadPath = setting.getStr("tmp.upload.path"); + fileExts = setting.getStrings("file.exts"); + isAllowFileExts = setting.getBool("file.exts.allow"); + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/servlet/multipart/package-info.java b/hutool-extra/src/main/java/cn/hutool/extra/servlet/multipart/package-info.java new file mode 100644 index 000000000..0328d88f6 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/servlet/multipart/package-info.java @@ -0,0 +1,7 @@ +/** + * 基于Servlet的文件上传封装 + * + * @author looly + * + */ +package cn.hutool.extra.servlet.multipart; \ No newline at end of file diff --git a/hutool-extra/src/main/java/cn/hutool/extra/servlet/package-info.java b/hutool-extra/src/main/java/cn/hutool/extra/servlet/package-info.java new file mode 100644 index 000000000..389f51b36 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/servlet/package-info.java @@ -0,0 +1,7 @@ +/** + * Servlet封装,包括Servlet参数获取、文件上传、Response写出等,入口为ServletUtil + * + * @author looly + * + */ +package cn.hutool.extra.servlet; \ No newline at end of file diff --git a/hutool-extra/src/main/java/cn/hutool/extra/ssh/ChannelType.java b/hutool-extra/src/main/java/cn/hutool/extra/ssh/ChannelType.java new file mode 100644 index 000000000..51b29f22e --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/ssh/ChannelType.java @@ -0,0 +1,49 @@ +package cn.hutool.extra.ssh; + +/** + * Jsch支持的Channel类型 + * + * @author looly + * @since 4.5.2 + */ +public enum ChannelType { + /** Session */ + SESSION("session"), + /** shell */ + SHELL("shell"), + /** exec */ + EXEC("exec"), + /** x11 */ + X11("x11"), + /** agent forwarding */ + AGENT_FORWARDING("auth-agent@openssh.com"), + /** direct tcpip */ + DIRECT_TCPIP("direct-tcpip"), + /** forwarded tcpip */ + FORWARDED_TCPIP("forwarded-tcpip"), + /** sftp */ + SFTP("sftp"), + /** subsystem */ + SUBSYSTEM("subsystem"); + + /** channel值 */ + private String value; + + /** + * 构造 + * + * @param value 类型值 + */ + private ChannelType(String value) { + this.value = value; + } + + /** + * 获取值 + * + * @return 值 + */ + public String getValue() { + return this.value; + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/ssh/Connector.java b/hutool-extra/src/main/java/cn/hutool/extra/ssh/Connector.java new file mode 100644 index 000000000..3bd8335f1 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/ssh/Connector.java @@ -0,0 +1,147 @@ +package cn.hutool.extra.ssh; + +/** + * 连接者对象,提供一些连接的基本信息 + * + * @author Luxiaolei + * + */ +public class Connector { + private String host; + private int port; + private String user; + private String password; + private String group; + + // ----------------------------------------------------------------------- 构造 start + public Connector() { + } + + /** + * 构造 + * + * @param user 用户名 + * @param password 密码 + * @param group 组 + */ + public Connector(String user, String password, String group) { + this.user = user; + this.password = password; + this.group = group; + } + + /** + * 构造 + * + * @param host 主机名 + * @param port 端口 + * @param user 用户名 + * @param password 密码 + */ + public Connector(String host, int port, String user, String password) { + super(); + this.host = host; + this.port = port; + this.user = user; + this.password = password; + } + // ----------------------------------------------------------------------- 构造 end + + /** + * 获得主机名 + * + * @return 主机名 + */ + public String getHost() { + return host; + } + + /** + * 设定主机名 + * + * @param host 主机名 + */ + public void setHost(String host) { + this.host = host; + } + + /** + * 获得端口号 + * + * @return 端口号 + */ + public int getPort() { + return port; + } + + /** + * 设定端口号 + * + * @param port 端口号 + */ + public void setPort(int port) { + this.port = port; + } + + /** + * 获得用户名 + * + * @return 用户名 + */ + public String getUser() { + return user; + } + + /** + * 设定用户名 + * + * @param name 用户名 + */ + public void setUser(String name) { + this.user = name; + } + + /** + * 获得密码 + * + * @return 密码 + */ + public String getPassword() { + return password; + } + + /** + * 设定密码 + * + * @param password 密码 + */ + public void setPassword(String password) { + this.password = password; + } + + /** + * 获得用户组名 + * + * @return 用户组 + */ + public String getGroup() { + return group; + } + + /** + * 设定用户组名 + * + * @param group 用户组 + */ + public void setGroup(String group) { + this.group = group; + } + + /** + * toString方法仅用于测试显示 + */ + @Override + public String toString() { + return "Connector [host=" + host + ", port=" + port + ", user=" + user + ", password=" + password + "]"; + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/ssh/JschRuntimeException.java b/hutool-extra/src/main/java/cn/hutool/extra/ssh/JschRuntimeException.java new file mode 100644 index 000000000..6c1017054 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/ssh/JschRuntimeException.java @@ -0,0 +1,32 @@ +package cn.hutool.extra.ssh; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.StrUtil; + +/** + * Jsch异常 + * @author xiaoleilu + */ +public class JschRuntimeException extends RuntimeException{ + private static final long serialVersionUID = 8247610319171014183L; + + public JschRuntimeException(Throwable e) { + super(ExceptionUtil.getMessage(e), e); + } + + public JschRuntimeException(String message) { + super(message); + } + + public JschRuntimeException(String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params)); + } + + public JschRuntimeException(String message, Throwable throwable) { + super(message, throwable); + } + + public JschRuntimeException(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/ssh/JschSessionPool.java b/hutool-extra/src/main/java/cn/hutool/extra/ssh/JschSessionPool.java new file mode 100644 index 000000000..50930d2a8 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/ssh/JschSessionPool.java @@ -0,0 +1,116 @@ +package cn.hutool.extra.ssh; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; + +import com.jcraft.jsch.Session; + +import cn.hutool.core.util.StrUtil; + +/** + * Jsch会话池 + * + * @author looly + * + */ +public enum JschSessionPool { + INSTANCE; + + /** SSH会话池,key:host,value:Session对象 */ + private Map sessionPool = new ConcurrentHashMap(); + /** 锁 */ + private static final Object lock = new Object(); + + /** + * 获取Session,不存在返回null + * + * @param key 键 + * @return Session + */ + public Session get(String key) { + return sessionPool.get(key); + } + + /** + * 获得一个SSH跳板机会话,重用已经使用的会话 + * + * @param sshHost 跳板机主机 + * @param sshPort 跳板机端口 + * @param sshUser 跳板机用户名 + * @param sshPass 跳板机密码 + * @return SSH会话 + */ + public Session getSession(String sshHost, int sshPort, String sshUser, String sshPass) { + final String key = StrUtil.format("{}@{}:{}", sshUser, sshHost, sshPort); + Session session = get(key); + if (null == session || false == session.isConnected()) { + synchronized (lock) { + session = get(key); + if (null == session || false == session.isConnected()) { + session = JschUtil.openSession(sshHost, sshPort, sshUser, sshPass); + put(key, session); + } + } + } + return session; + } + + /** + * 加入Session + * + * @param key 键 + * @param session Session + */ + public void put(String key, Session session) { + this.sessionPool.put(key, session); + } + + /** + * 关闭SSH连接会话 + * + * @param key 主机,格式为user@host:port + */ + public void close(String key) { + Session session = sessionPool.get(key); + if (session != null && session.isConnected()) { + session.disconnect(); + } + sessionPool.remove(key); + } + + /** + * 移除指定Session + * + * @param session Session会话 + * @since 4.1.15 + */ + public void remove(Session session) { + if(null != session) { + final Iterator> iterator = this.sessionPool.entrySet().iterator(); + Entry entry; + while(iterator.hasNext()) { + entry = iterator.next(); + if(session.equals(entry.getValue())) { + iterator.remove(); + break; + } + } + } + } + + /** + * 关闭所有SSH连接会话 + */ + public void closeAll() { + Collection sessions = sessionPool.values(); + for (Session session : sessions) { + if (session.isConnected()) { + session.disconnect(); + } + } + sessionPool.clear(); + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/ssh/JschUtil.java b/hutool-extra/src/main/java/cn/hutool/extra/ssh/JschUtil.java new file mode 100644 index 000000000..918546980 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/ssh/JschUtil.java @@ -0,0 +1,335 @@ +package cn.hutool.extra.ssh; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; + +import com.jcraft.jsch.Channel; +import com.jcraft.jsch.ChannelExec; +import com.jcraft.jsch.ChannelSftp; +import com.jcraft.jsch.ChannelShell; +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.Session; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.net.LocalPortGenerater; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; + +/** + * Jsch工具类
+ * Jsch是Java Secure Channel的缩写。JSch是一个SSH2的纯Java实现。
+ * 它允许你连接到一个SSH服务器,并且可以使用端口转发,X11转发,文件传输等。
+ * + * @author Looly + * @since 4.0.0 + */ +public class JschUtil { + + /** 不使用SSH的值 */ + public final static String SSH_NONE = "none"; + + /** 本地端口生成器 */ + private static final LocalPortGenerater portGenerater = new LocalPortGenerater(10000); + + /** + * 生成一个本地端口,用于远程端口映射 + * + * @return 未被使用的本地端口 + */ + public static int generateLocalPort() { + return portGenerater.generate(); + } + + /** + * 获得一个SSH会话,重用已经使用的会话 + * + * @param sshHost 主机 + * @param sshPort 端口 + * @param sshUser 用户名 + * @param sshPass 密码 + * @return SSH会话 + */ + public static Session getSession(String sshHost, int sshPort, String sshUser, String sshPass) { + return JschSessionPool.INSTANCE.getSession(sshHost, sshPort, sshUser, sshPass); + } + + /** + * 打开一个新的SSH会话 + * + * @param sshHost 主机 + * @param sshPort 端口 + * @param sshUser 用户名 + * @param sshPass 密码 + * @return SSH会话 + */ + public static Session openSession(String sshHost, int sshPort, String sshUser, String sshPass) { + final Session session = createSession(sshHost, sshPort, sshUser, sshPass); + try { + session.connect(); + } catch (JSchException e) { + throw new JschRuntimeException(e); + } + return session; + } + + /** + * 新建一个新的SSH会话 + * + * @param sshHost 主机 + * @param sshPort 端口 + * @param sshUser 机用户名 + * @param sshPass 密码 + * @return SSH会话 + * @since 4.5.2 + */ + public static Session createSession(String sshHost, int sshPort, String sshUser, String sshPass) { + if (StrUtil.isEmpty(sshHost) || sshPort < 0 || StrUtil.isEmpty(sshUser) || StrUtil.isEmpty(sshPass)) { + return null; + } + + Session session; + try { + session = new JSch().getSession(sshUser, sshHost, sshPort); + session.setPassword(sshPass); + // 设置第一次登陆的时候提示,可选值:(ask | yes | no) + session.setConfig("StrictHostKeyChecking", "no"); + } catch (JSchException e) { + throw new JschRuntimeException(e); + } + return session; + } + + /** + * 绑定端口到本地。 一个会话可绑定多个端口 + * + * @param session 需要绑定端口的SSH会话 + * @param remoteHost 远程主机 + * @param remotePort 远程端口 + * @param localPort 本地端口 + * @return 成功与否 + * @throws JschRuntimeException 端口绑定失败异常 + */ + public static boolean bindPort(Session session, String remoteHost, int remotePort, int localPort) throws JschRuntimeException { + if (session != null && session.isConnected()) { + try { + session.setPortForwardingL(localPort, remoteHost, remotePort); + } catch (JSchException e) { + throw new JschRuntimeException(e, "From [{}] mapping to [{}] error!", remoteHost, localPort); + } + return true; + } + return false; + } + + /** + * 解除端口映射 + * + * @param session 需要解除端口映射的SSH会话 + * @param localPort 需要解除的本地端口 + * @return 解除成功与否 + */ + public static boolean unBindPort(Session session, int localPort) { + try { + session.delPortForwardingL(localPort); + return true; + } catch (JSchException e) { + throw new JschRuntimeException(e); + } + } + + /** + * 打开SSH会话,并绑定远程端口到本地的一个随机端口 + * + * @param sshConn SSH连接信息对象 + * @param remoteHost 远程主机 + * @param remotePort 远程端口 + * @return 映射后的本地端口 + * @throws JschRuntimeException 连接异常 + */ + public static int openAndBindPortToLocal(Connector sshConn, String remoteHost, int remotePort) throws JschRuntimeException { + final Session session = openSession(sshConn.getHost(), sshConn.getPort(), sshConn.getUser(), sshConn.getPassword()); + if (session == null) { + throw new JschRuntimeException("Error to create SSH Session!"); + } + final int localPort = generateLocalPort(); + bindPort(session, remoteHost, remotePort, localPort); + return localPort; + } + + /** + * 打开SFTP连接 + * + * @param session Session会话 + * @return {@link ChannelSftp} + * @since 4.0.3 + */ + public static ChannelSftp openSftp(Session session) { + return (ChannelSftp) openChannel(session, ChannelType.SFTP); + } + + /** + * 创建Sftp + * + * @param sshHost 远程主机 + * @param sshPort 远程主机端口 + * @param sshUser 远程主机用户名 + * @param sshPass 远程主机密码 + * @return {@link Sftp} + * @since 4.0.3 + */ + public static Sftp createSftp(String sshHost, int sshPort, String sshUser, String sshPass) { + return new Sftp(sshHost, sshPort, sshUser, sshPass); + } + + /** + * 创建Sftp + * + * @param session SSH会话 + * @return {@link Sftp} + * @since 4.0.5 + */ + public static Sftp createSftp(Session session) { + return new Sftp(session); + } + + /** + * 打开Shell连接 + * + * @param session Session会话 + * @return {@link ChannelShell} + * @since 4.0.3 + */ + public static ChannelShell openShell(Session session) { + return (ChannelShell) openChannel(session, ChannelType.SHELL); + } + + /** + * 打开Channel连接 + * + * @param session Session会话 + * @param channelType 通道类型,可以是shell或sftp等,见{@link ChannelType} + * @return {@link Channel} + * @since 4.5.2 + */ + public static Channel openChannel(Session session, ChannelType channelType) { + final Channel channel = createChannel(session, channelType); + try { + channel.connect(); + } catch (JSchException e) { + throw new JschRuntimeException(e); + } + return channel; + } + + /** + * 创建Channel连接 + * + * @param session Session会话 + * @param channelType 通道类型,可以是shell或sftp等,见{@link ChannelType} + * @return {@link Channel} + * @since 4.5.2 + */ + public static Channel createChannel(Session session, ChannelType channelType) { + Channel channel; + try { + if (false == session.isConnected()) { + session.connect(); + } + channel = session.openChannel(channelType.getValue()); + } catch (JSchException e) { + throw new JschRuntimeException(e); + } + return channel; + } + + /** + * 执行Shell命令 + * + * @param session Session会话 + * @param cmd 命令 + * @param charset 发送和读取内容的编码 + * @return {@link ChannelExec} + * @since 4.0.3 + */ + public static String exec(Session session, String cmd, Charset charset) { + return exec(session, cmd, charset, System.err); + } + + /** + * 执行Shell命令 + * + * @param session Session会话 + * @param cmd 命令 + * @param charset 发送和读取内容的编码 + * @param errStream 错误信息输出到的位置 + * @return {@link ChannelExec} + * @since 4.3.1 + */ + public static String exec(Session session, String cmd, Charset charset, OutputStream errStream) { + if (null == charset) { + charset = CharsetUtil.CHARSET_UTF_8; + } + ChannelExec channel = (ChannelExec) openChannel(session, ChannelType.EXEC); + channel.setCommand(StrUtil.bytes(cmd, charset)); + channel.setInputStream(null); + channel.setErrStream(errStream); + InputStream in = null; + try { + channel.start(); + in = channel.getInputStream(); + return IoUtil.read(in, CharsetUtil.CHARSET_UTF_8); + } catch (IOException e) { + throw new IORuntimeException(e); + } catch (JSchException e) { + throw new IORuntimeException(e); + } finally { + IoUtil.close(in); + close(channel); + } + } + + /** + * 关闭SSH连接会话 + * + * @param session SSH会话 + */ + public static void close(Session session) { + if (session != null && session.isConnected()) { + session.disconnect(); + } + JschSessionPool.INSTANCE.remove(session); + } + + /** + * 关闭会话通道 + * + * @param channel 会话通道 + * @since 4.0.3 + */ + public static void close(Channel channel) { + if (channel != null && channel.isConnected()) { + channel.disconnect(); + } + } + + /** + * 关闭SSH连接会话 + * + * @param key 主机,格式为user@host:port + */ + public static void close(String key) { + JschSessionPool.INSTANCE.close(key); + } + + /** + * 关闭所有SSH连接会话 + */ + public static void closeAll() { + JschSessionPool.INSTANCE.closeAll(); + } + +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/ssh/Sftp.java b/hutool-extra/src/main/java/cn/hutool/extra/ssh/Sftp.java new file mode 100644 index 000000000..459616fd8 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/ssh/Sftp.java @@ -0,0 +1,425 @@ +package cn.hutool.extra.ssh; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.Vector; + +import com.jcraft.jsch.ChannelSftp; +import com.jcraft.jsch.ChannelSftp.LsEntry; +import com.jcraft.jsch.ChannelSftp.LsEntrySelector; +import com.jcraft.jsch.Session; +import com.jcraft.jsch.SftpException; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.lang.Filter; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.ftp.AbstractFtp; + +/** + * SFTP是Secure File Transfer Protocol的缩写,安全文件传送协议。可以为传输文件提供一种安全的加密方法。
+ * SFTP 为 SSH的一部份,是一种传输文件到服务器的安全方式。SFTP是使用加密传输认证信息和传输的数据,所以,使用SFTP是非常安全的。
+ * 但是,由于这种传输方式使用了加密/解密技术,所以传输效率比普通的FTP要低得多,如果您对网络安全性要求更高时,可以使用SFTP代替FTP。
+ * + *

+ * 此类为基于jsch的SFTP实现
+ * 参考:https://www.cnblogs.com/longyg/archive/2012/06/25/2556576.html + *

+ * + * @author looly + * @since 4.0.2 + */ +public class Sftp extends AbstractFtp { + + private Session session; + private ChannelSftp channel; + + // ---------------------------------------------------------------------------------------- Constructor start + /** + * 构造 + * + * @param sshHost 远程主机 + * @param sshPort 远程主机端口 + * @param sshUser 远程主机用户名 + * @param sshPass 远程主机密码 + */ + public Sftp(String sshHost, int sshPort, String sshUser, String sshPass) { + this(sshHost, sshPort, sshUser, sshPass, DEFAULT_CHARSET); + } + + /** + * 构造 + * + * @param sshHost 远程主机 + * @param sshPort 远程主机端口 + * @param sshUser 远程主机用户名 + * @param sshPass 远程主机密码 + * @param charset 编码 + * @since 4.1.14 + */ + public Sftp(String sshHost, int sshPort, String sshUser, String sshPass, Charset charset) { + init(sshHost, sshPort, sshUser, sshPass, charset); + } + + /** + * 构造 + * + * @param session {@link Session} + */ + public Sftp(Session session) { + this(session, DEFAULT_CHARSET); + } + + /** + * 构造 + * + * @param session {@link Session} + * @param charset 编码 + * @since 4.1.14 + */ + public Sftp(Session session, Charset charset) { + init(session, charset); + } + + /** + * 构造 + * + * @param channel {@link ChannelSftp} + * @param charset 编码 + */ + public Sftp(ChannelSftp channel, Charset charset) { + init(channel, charset); + } + // ---------------------------------------------------------------------------------------- Constructor end + + /** + * 构造 + * + * @param sshHost 远程主机 + * @param sshPort 远程主机端口 + * @param sshUser 远程主机用户名 + * @param sshPass 远程主机密码 + * @param charset 编码 + */ + public void init(String sshHost, int sshPort, String sshUser, String sshPass, Charset charset) { + this.host = sshHost; + this.port = sshPort; + this.user = sshUser; + this.password = sshPass; + init(JschUtil.getSession(sshHost, sshPort, sshUser, sshPass), charset); + } + + /** + * 初始化 + * + * @param session {@link Session} + * @param charset 编码 + */ + public void init(Session session, Charset charset) { + this.session = session; + init(JschUtil.openSftp(session), charset); + } + + /** + * 初始化 + * + * @param channel {@link ChannelSftp} + * @param charset 编码 + */ + public void init(ChannelSftp channel, Charset charset) { + this.charset = charset; + try { + channel.setFilenameEncoding(charset.toString()); + } catch (SftpException e) { + throw new JschRuntimeException(e); + } + this.channel = channel; + } + + @Override + public Sftp reconnectIfTimeout() { + if (false == this.cd("/") && StrUtil.isNotBlank(this.host)) { + init(this.host, this.port, this.user, this.password, this.charset); + } + return this; + } + + /** + * 获取SFTP通道客户端 + * + * @return 通道客户端 + * @since 4.1.14 + */ + public ChannelSftp getClient() { + return this.channel; + } + + /** + * 远程当前目录 + * + * @return 远程当前目录 + */ + @Override + public String pwd() { + try { + return channel.pwd(); + } catch (SftpException e) { + throw new JschRuntimeException(e); + } + } + + /** + * 获取HOME路径 + * + * @return HOME路径 + * @since 4.0.5 + */ + public String home() { + try { + return channel.getHome(); + } catch (SftpException e) { + throw new JschRuntimeException(e); + } + } + + /** + * 遍历某个目录下所有文件或目录,不会递归遍历 + * + * @param path 遍历某个目录下所有文件或目录 + * @return 目录或文件名列表 + * @since 4.0.5 + */ + @Override + public List ls(String path) { + return ls(path, null); + } + + /** + * 遍历某个目录下所有目录,不会递归遍历 + * + * @param path 遍历某个目录下所有目录 + * @return 目录名列表 + * @since 4.0.5 + */ + public List lsDirs(String path) { + return ls(path, new Filter() { + @Override + public boolean accept(LsEntry t) { + return t.getAttrs().isDir(); + } + }); + } + + /** + * 遍历某个目录下所有文件,不会递归遍历 + * + * @param path 遍历某个目录下所有文件 + * @return 文件名列表 + * @since 4.0.5 + */ + public List lsFiles(String path) { + return ls(path, new Filter() { + @Override + public boolean accept(LsEntry t) { + return false == t.getAttrs().isDir(); + } + }); + } + + /** + * 遍历某个目录下所有文件或目录,不会递归遍历 + * + * @param path 遍历某个目录下所有文件或目录 + * @param filter 文件或目录过滤器,可以实现过滤器返回自己需要的文件或目录名列表 + * @return 目录或文件名列表 + * @since 4.0.5 + */ + public List ls(String path, final Filter filter) { + final List fileNames = new ArrayList<>(); + try { + channel.ls(path, new LsEntrySelector() { + @Override + public int select(LsEntry entry) { + String fileName = entry.getFilename(); + if (false == StrUtil.equals(".", fileName) && false == StrUtil.equals("..", fileName)) { + if (null == filter || filter.accept(entry)) { + fileNames.add(entry.getFilename()); + } + } + return CONTINUE; + } + }); + } catch (SftpException e) { + throw new JschRuntimeException(e); + } + return fileNames; + } + + @Override + public boolean mkdir(String dir) { + try { + this.channel.mkdir(dir); + return true; + } catch (SftpException e) { + throw new JschRuntimeException(e); + } + } + + /** + * 打开指定目录,如果指定路径非目录或不存在返回false + * + * @param directory directory + * @return 是否打开目录 + */ + @Override + public boolean cd(String directory) { + if (StrUtil.isBlank(directory)) { + // 当前目录 + return true; + } + try { + channel.cd(directory.replaceAll("\\\\", "/")); + return true; + } catch (SftpException e) { + return false; + } + } + + /** + * 删除文件 + * + * @param filePath 要删除的文件绝对路径 + */ + @Override + public boolean delFile(String filePath) { + try { + channel.rm(filePath); + } catch (SftpException e) { + throw new JschRuntimeException(e); + } + return true; + } + + /** + * 删除文件夹及其文件夹下的所有文件 + * + * @param dirPath 文件夹路径 + * @return boolean 是否删除成功 + */ + @Override + @SuppressWarnings("unchecked") + public boolean delDir(String dirPath) { + if (false == cd(dirPath)) { + return false; + } + + Vector list = null; + try { + list = channel.ls(channel.pwd()); + } catch (SftpException e) { + throw new JschRuntimeException(e); + } + + String fileName; + for (LsEntry entry : list) { + fileName = entry.getFilename(); + if (false == fileName.equals(".") && false == fileName.equals("..")) { + if (entry.getAttrs().isDir()) { + delDir(fileName); + } else { + delFile(fileName); + } + } + } + + if (false == cd("..")) { + return false; + } + + // 删除空目录 + try { + channel.rmdir(dirPath); + return true; + } catch (SftpException e) { + throw new JschRuntimeException(e); + } + } + + @Override + public boolean upload(String srcFilePath, File destFile) { + put(srcFilePath, FileUtil.getAbsolutePath(destFile)); + return true; + } + + /** + * 将本地文件上传到目标服务器,目标文件名为destPath,若destPath为目录,则目标文件名将与srcFilePath文件名相同。覆盖模式 + * + * @param srcFilePath 本地文件路径 + * @param destPath 目标路径, + * @return this + */ + public Sftp put(String srcFilePath, String destPath) { + return put(srcFilePath, destPath, Mode.OVERWRITE); + } + + /** + * 将本地文件上传到目标服务器,目标文件名为destPath,若destPath为目录,则目标文件名将与srcFilePath文件名相同。 + * + * @param srcFilePath 本地文件路径 + * @param destPath 目标路径, + * @param mode {@link Mode} 模式 + * @return this + */ + public Sftp put(String srcFilePath, String destPath, Mode mode) { + try { + channel.put(srcFilePath, destPath, mode.ordinal()); + } catch (SftpException e) { + throw new JschRuntimeException(e); + } + return this; + } + + @Override + public void download(String src, File destFile) { + get(src, FileUtil.getAbsolutePath(destFile)); + } + + /** + * 获取远程文件 + * + * @param src 远程文件路径 + * @param dest 目标文件路径 + * @return this + */ + public Sftp get(String src, String dest) { + try { + channel.get(src, dest); + } catch (SftpException e) { + throw new JschRuntimeException(e); + } + return this; + } + + @Override + public void close() throws IOException { + JschUtil.close(this.channel); + JschUtil.close(this.session); + } + + /** + * JSch支持的三种文件传输模式 + * + * @author looly + * + */ + public static enum Mode { + /** 完全覆盖模式,这是JSch的默认文件传输模式,即如果目标文件已经存在,传输的文件将完全覆盖目标文件,产生新的文件。 */ + OVERWRITE, + /** 恢复模式,如果文件已经传输一部分,这时由于网络或其他任何原因导致文件传输中断,如果下一次传输相同的文件,则会从上一次中断的地方续传。 */ + RESUME, + /** 追加模式,如果目标文件已存在,传输的文件将在目标文件后追加。 */ + APPEND; + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/ssh/package-info.java b/hutool-extra/src/main/java/cn/hutool/extra/ssh/package-info.java new file mode 100644 index 000000000..340cdc231 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/ssh/package-info.java @@ -0,0 +1,7 @@ +/** + * Jsch封装,包括端口映射、SFTP封装等,入口为JschUtil + * + * @author looly + * + */ +package cn.hutool.extra.ssh; \ No newline at end of file diff --git a/hutool-extra/src/main/java/cn/hutool/extra/template/AbstractTemplate.java b/hutool-extra/src/main/java/cn/hutool/extra/template/AbstractTemplate.java new file mode 100644 index 000000000..bab809bf4 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/template/AbstractTemplate.java @@ -0,0 +1,36 @@ +package cn.hutool.extra.template; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.StringWriter; +import java.util.Map; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IoUtil; + +/** + * 抽象模板,提供将模板融合后写出到文件、返回字符串等方法 + * + * @author looly + * + */ +public abstract class AbstractTemplate implements Template{ + + @Override + public void render(Map bindingMap, File file) { + BufferedOutputStream out = null; + try { + out = FileUtil.getOutputStream(file); + this.render(bindingMap, out); + } finally { + IoUtil.close(out); + } + } + + @Override + public String render(Map bindingMap) { + final StringWriter writer = new StringWriter(); + render(bindingMap, writer); + return writer.toString(); + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/template/Template.java b/hutool-extra/src/main/java/cn/hutool/extra/template/Template.java new file mode 100644 index 000000000..be7d86f7f --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/template/Template.java @@ -0,0 +1,46 @@ +package cn.hutool.extra.template; + +import java.io.File; +import java.io.OutputStream; +import java.io.Writer; +import java.util.Map; + +/** + * 抽象模板接口 + * + * @author looly + * + */ +public interface Template { + + /** + * 将模板与绑定参数融合后输出到Writer + * + * @param bindingMap 绑定的参数,此Map中的参数会替换模板中的变量 + * @param writer 输出 + */ + void render(Map bindingMap, Writer writer); + + /** + * 将模板与绑定参数融合后输出到流 + * + * @param bindingMap 绑定的参数,此Map中的参数会替换模板中的变量 + * @param out 输出 + */ + void render(Map bindingMap, OutputStream out); + + /** + * 写出到文件 + * @param bindingMap 绑定的参数,此Map中的参数会替换模板中的变量 + * @param file 输出到的文件 + */ + void render(Map bindingMap, File file); + + /** + * 将模板与绑定参数融合后返回为字符串 + * + * @param bindingMap 绑定的参数,此Map中的参数会替换模板中的变量 + * @return 融合后的内容 + */ + String render(Map bindingMap); +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/template/TemplateConfig.java b/hutool-extra/src/main/java/cn/hutool/extra/template/TemplateConfig.java new file mode 100644 index 000000000..e60ad6095 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/template/TemplateConfig.java @@ -0,0 +1,189 @@ +package cn.hutool.extra.template; + +import java.io.Serializable; +import java.nio.charset.Charset; + +import cn.hutool.core.util.CharsetUtil; + +/** + * 模板配置 + * + * @author looly + * @since 4.1.0 + */ +public class TemplateConfig implements Serializable { + private static final long serialVersionUID = 2933113779920339523L; + + /** 编码 */ + private Charset charset; + /** 模板路径,如果ClassPath或者WebRoot模式,则表示相对路径 */ + private String path; + /** 模板资源加载方式 */ + private ResourceMode resourceMode; + + /** + * 默认构造,使用UTF8编码,默认从ClassPath获取模板 + */ + public TemplateConfig() { + this((String)null); + } + + /** + * 构造,默认UTF-8编码 + * + * @param path 模板路径,如果ClassPath或者WebRoot模式,则表示相对路径 + */ + public TemplateConfig(String path) { + this(path, ResourceMode.STRING); + } + + /** + * 构造,默认UTF-8编码 + * + * @param path 模板路径,如果ClassPath或者WebRoot模式,则表示相对路径 + * @param resourceMode 模板资源加载方式 + */ + public TemplateConfig(String path, ResourceMode resourceMode) { + this(CharsetUtil.CHARSET_UTF_8, path, resourceMode); + } + + /** + * 构造 + * + * @param charset 编码 + * @param path 模板路径,如果ClassPath或者WebRoot模式,则表示相对路径 + * @param resourceMode 模板资源加载方式 + */ + public TemplateConfig(Charset charset, String path, ResourceMode resourceMode) { + this.charset = charset; + this.path = path; + this.resourceMode = resourceMode; + } + + /** + * 获取编码 + * + * @return 编码 + */ + public Charset getCharset() { + return charset; + } + + /** + * 获取编码 + * + * @return 编码 + * @since 4.1.11 + */ + public String getCharsetStr() { + if(null == this.charset) { + return null; + } + return this.charset.toString(); + } + + /** + * 设置编码 + * + * @param charset 编码 + */ + public void setCharset(Charset charset) { + this.charset = charset; + } + + /** + * 获取模板路径,如果ClassPath或者WebRoot模式,则表示相对路径 + * + * @return 模板路径 + */ + public String getPath() { + return path; + } + + /** + * 设置模板路径,如果ClassPath或者WebRoot模式,则表示相对路径 + * + * @param path 模板路径 + */ + public void setPath(String path) { + this.path = path; + } + + /** + * 获取模板资源加载方式 + * + * @return 模板资源加载方式 + */ + public ResourceMode getResourceMode() { + return resourceMode; + } + + /** + * 设置模板资源加载方式 + * + * @param resourceMode 模板资源加载方式 + */ + public void setResourceMode(ResourceMode resourceMode) { + this.resourceMode = resourceMode; + } + + /** + * 资源加载方式枚举 + * + * @author looly + */ + public static enum ResourceMode { + /** 从ClassPath加载模板 */ + CLASSPATH, + /** 从File目录加载模板 */ + FILE, + /** 从WebRoot目录加载模板 */ + WEB_ROOT, + /** 从模板文本加载模板 */ + STRING, + /** 复合加载模板(分别从File、ClassPath、Web-root、String方式尝试查找模板) */ + COMPOSITE; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((charset == null) ? 0 : charset.hashCode()); + result = prime * result + ((path == null) ? 0 : path.hashCode()); + result = prime * result + ((resourceMode == null) ? 0 : resourceMode.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; + } + TemplateConfig other = (TemplateConfig) obj; + if (charset == null) { + if (other.charset != null) { + return false; + } + } else if (!charset.equals(other.charset)) { + return false; + } + if (path == null) { + if (other.path != null) { + return false; + } + } else if (!path.equals(other.path)) { + return false; + } + if (resourceMode != other.resourceMode) { + return false; + } + return true; + }; +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/template/TemplateEngine.java b/hutool-extra/src/main/java/cn/hutool/extra/template/TemplateEngine.java new file mode 100644 index 000000000..0510d3057 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/template/TemplateEngine.java @@ -0,0 +1,18 @@ +package cn.hutool.extra.template; + +/** + * 引擎接口,通过实现此接口从而使用对应的模板引擎 + * + * @author looly + */ +public interface TemplateEngine { + + /** + * 获取模板 + * + * @param resource 资源,根据事先不同,此资源可以是模板本身,也可以是模板的相对路径 + * @return 模板实现 + */ + Template getTemplate(String resource); + +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/template/TemplateException.java b/hutool-extra/src/main/java/cn/hutool/extra/template/TemplateException.java new file mode 100644 index 000000000..da100ba21 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/template/TemplateException.java @@ -0,0 +1,33 @@ +package cn.hutool.extra.template; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 模板异常 + * + * @author xiaoleilu + */ +public class TemplateException extends RuntimeException { + private static final long serialVersionUID = 8247610319171014183L; + + public TemplateException(Throwable e) { + super(ExceptionUtil.getMessage(e), e); + } + + public TemplateException(String message) { + super(message); + } + + public TemplateException(String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params)); + } + + public TemplateException(String message, Throwable throwable) { + super(message, throwable); + } + + public TemplateException(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/template/TemplateUtil.java b/hutool-extra/src/main/java/cn/hutool/extra/template/TemplateUtil.java new file mode 100644 index 000000000..bb0cc8354 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/template/TemplateUtil.java @@ -0,0 +1,34 @@ +package cn.hutool.extra.template; + +import cn.hutool.extra.template.engine.TemplateFactory; + +/** + * 模板工具类 + * + * @author looly + * @since 4.1.0 + */ +public class TemplateUtil { + + /** + * 根据用户引入的模板引擎jar,自动创建对应的模板引擎对象,使用默认配置
+ * 推荐创建的引擎单例使用,此方法每次调用会返回新的引擎 + * + * @return {@link TemplateEngine} + * @since 4.1.11 + */ + public static TemplateEngine createEngine() { + return createEngine(new TemplateConfig()); + } + + /** + * 根据用户引入的模板引擎jar,自动创建对应的模板引擎对象
+ * 推荐创建的引擎单例使用,此方法每次调用会返回新的引擎 + * + * @param config 模板配置,包括编码、模板文件path等信息 + * @return {@link TemplateEngine} + */ + public static TemplateEngine createEngine(TemplateConfig config) { + return TemplateFactory.create(config); + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/template/engine/TemplateFactory.java b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/TemplateFactory.java new file mode 100644 index 000000000..c60259021 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/TemplateFactory.java @@ -0,0 +1,77 @@ +package cn.hutool.extra.template.engine; + +import com.jfinal.template.Engine; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; +import cn.hutool.extra.template.TemplateException; +import cn.hutool.extra.template.engine.beetl.BeetlEngine; +import cn.hutool.extra.template.engine.enjoy.EnjoyEngine; +import cn.hutool.extra.template.engine.freemarker.FreemarkerEngine; +import cn.hutool.extra.template.engine.rythm.RythmEngine; +import cn.hutool.extra.template.engine.thymeleaf.ThymeleafEngine; +import cn.hutool.extra.template.engine.velocity.VelocityEngine; +import cn.hutool.log.StaticLog; + +/** + * 简单模板工厂,用于根据用户引入的模板引擎jar,自动创建对应的模板引擎对象 + * + * @author looly + * + */ +public class TemplateFactory { + /** + * 根据用户引入的模板引擎jar,自动创建对应的模板引擎对象
+ * 推荐创建的引擎单例使用,此方法每次调用会返回新的引擎 + * + * @param config 模板配置,包括编码、模板文件path等信息 + * @return {@link Engine} + */ + public static TemplateEngine create(TemplateConfig config) { + final TemplateEngine engine = doCreate(config); + StaticLog.debug("Use [{}] Engine As Default.", StrUtil.removeSuffix(engine.getClass().getSimpleName(), "Engine")); + return engine; + } + + /** + * 根据用户引入的模板引擎jar,自动创建对应的模板引擎对象
+ * 推荐创建的引擎单例使用,此方法每次调用会返回新的引擎 + * + * @param config 模板配置,包括编码、模板文件path等信息 + * @return {@link Engine} + */ + private static TemplateEngine doCreate(TemplateConfig config) { + try { + return new BeetlEngine(config); + } catch (NoClassDefFoundError e) { + // ignore + } + try { + return new FreemarkerEngine(config); + } catch (NoClassDefFoundError e) { + // ignore + } + try { + return new VelocityEngine(config); + } catch (NoClassDefFoundError e) { + // ignore + } + try { + return new RythmEngine(config); + } catch (NoClassDefFoundError e) { + // ignore + } + try { + return new EnjoyEngine(config); + } catch (NoClassDefFoundError e) { + // ignore + } + try { + return new ThymeleafEngine(config); + } catch (NoClassDefFoundError e) { + // ignore + } + throw new TemplateException("No template found ! Please add one of template jar to your project !"); + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/template/engine/beetl/BeetlEngine.java b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/beetl/BeetlEngine.java new file mode 100644 index 000000000..bde01f71d --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/beetl/BeetlEngine.java @@ -0,0 +1,114 @@ +package cn.hutool.extra.template.engine.beetl; + +import java.io.IOException; + +import org.beetl.core.Configuration; +import org.beetl.core.GroupTemplate; +import org.beetl.core.ResourceLoader; +import org.beetl.core.resource.ClasspathResourceLoader; +import org.beetl.core.resource.CompositeResourceLoader; +import org.beetl.core.resource.FileResourceLoader; +import org.beetl.core.resource.StringTemplateResourceLoader; +import org.beetl.core.resource.WebAppResourceLoader; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.extra.template.Template; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; + +/** + * Beetl模板引擎封装 + * + * @author looly + */ +public class BeetlEngine implements TemplateEngine { + + private GroupTemplate engine; + + // --------------------------------------------------------------------------------- Constructor start + /** + * 默认构造 + */ + public BeetlEngine() { + this(new TemplateConfig()); + } + + /** + * 构造 + * + * @param config 模板配置 + */ + public BeetlEngine(TemplateConfig config) { + this(createEngine(config)); + } + + /** + * 构造 + * + * @param engine {@link GroupTemplate} + */ + public BeetlEngine(GroupTemplate engine) { + this.engine = engine; + } + // --------------------------------------------------------------------------------- Constructor end + + @Override + public Template getTemplate(String resource) { + return BeetlTemplate.wrap(engine.getTemplate(resource)); + } + + /** + * 创建引擎 + * + * @param config 模板配置 + * @return {@link GroupTemplate} + */ + private static GroupTemplate createEngine(TemplateConfig config) { + if (null == config) { + config = new TemplateConfig(); + } + + switch (config.getResourceMode()) { + case CLASSPATH: + return createGroupTemplate(new ClasspathResourceLoader(config.getPath(), config.getCharsetStr())); + case FILE: + return createGroupTemplate(new FileResourceLoader(config.getPath(), config.getCharsetStr())); + case WEB_ROOT: + return createGroupTemplate(new WebAppResourceLoader(config.getPath(), config.getCharsetStr())); + case STRING: + return createGroupTemplate(new StringTemplateResourceLoader()); + case COMPOSITE: + //TODO 需要定义复合资源加载器 + return createGroupTemplate(new CompositeResourceLoader()); + default: + return new GroupTemplate(); + } + } + + /** + * 创建自定义的模板组 {@link GroupTemplate},配置文件使用全局默认
+ * 此时自定义的配置文件可在ClassPath中放入beetl.properties配置 + * + * @param loader {@link ResourceLoader},资源加载器 + * @return {@link GroupTemplate} + * @since 3.2.0 + */ + private static GroupTemplate createGroupTemplate(ResourceLoader loader) { + try { + return createGroupTemplate(loader, Configuration.defaultConfiguration()); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 创建自定义的 {@link GroupTemplate} + * + * @param loader {@link ResourceLoader},资源加载器 + * @param conf {@link Configuration} 配置文件 + * @return {@link GroupTemplate} + */ + private static GroupTemplate createGroupTemplate(ResourceLoader loader, Configuration conf) { + return new GroupTemplate(loader, conf); + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/template/engine/beetl/BeetlTemplate.java b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/beetl/BeetlTemplate.java new file mode 100644 index 000000000..030b42f8a --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/beetl/BeetlTemplate.java @@ -0,0 +1,51 @@ +package cn.hutool.extra.template.engine.beetl; + +import java.io.OutputStream; +import java.io.Serializable; +import java.io.Writer; +import java.util.Map; + +import cn.hutool.extra.template.AbstractTemplate; + +/** + * Beetl模板实现 + * + * @author looly + */ +public class BeetlTemplate extends AbstractTemplate implements Serializable{ + private static final long serialVersionUID = -8157926902932567280L; + + org.beetl.core.Template rawTemplate; + + /** + * 包装Beetl模板 + * + * @param beetlTemplate Beetl的模板对象 {@link org.beetl.core.Template} + * @return {@link BeetlTemplate} + */ + public static BeetlTemplate wrap(org.beetl.core.Template beetlTemplate) { + return (null == beetlTemplate) ? null : new BeetlTemplate(beetlTemplate); + } + + /** + * 构造 + * + * @param beetlTemplate Beetl的模板对象 {@link org.beetl.core.Template} + */ + public BeetlTemplate(org.beetl.core.Template beetlTemplate) { + this.rawTemplate = beetlTemplate; + } + + @Override + public void render(Map bindingMap, Writer writer) { + rawTemplate.binding(bindingMap); + rawTemplate.renderTo(writer); + } + + @Override + public void render(Map bindingMap, OutputStream out) { + rawTemplate.binding(bindingMap); + rawTemplate.renderTo(out); + } + +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/template/engine/beetl/BeetlUtil.java b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/beetl/BeetlUtil.java new file mode 100644 index 000000000..0b43e0a22 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/beetl/BeetlUtil.java @@ -0,0 +1,289 @@ +package cn.hutool.extra.template.engine.beetl; + +import java.io.IOException; +import java.io.Writer; +import java.nio.charset.Charset; +import java.util.Map; + +import org.beetl.core.Configuration; +import org.beetl.core.GroupTemplate; +import org.beetl.core.ResourceLoader; +import org.beetl.core.Template; +import org.beetl.core.resource.ClasspathResourceLoader; +import org.beetl.core.resource.CompositeResourceLoader; +import org.beetl.core.resource.FileResourceLoader; +import org.beetl.core.resource.Matcher; +import org.beetl.core.resource.StringTemplateResourceLoader; +import org.beetl.core.resource.WebAppResourceLoader; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.util.CharsetUtil; + +/** + * Beetl模板引擎工具类
+ * http://git.oschina.net/xiandafu/beetl2.0
+ * 文档:http://ibeetl.com/guide/beetl.html + * + * @author Looly + */ +public final class BeetlUtil { + + /** + * 创建默认模板组{@link GroupTemplate},默认的模板组从ClassPath中读取 + * + * @return {@link GroupTemplate} + */ + public static GroupTemplate createGroupTemplate() { + return new GroupTemplate(); + } + + /** + * 创建字符串的模板组 {@link GroupTemplate},配置文件使用全局默认
+ * 此时自定义的配置文件可在ClassPath中放入beetl.properties配置 + * + * @return {@link GroupTemplate} + * @since 3.2.0 + */ + public static GroupTemplate createStrGroupTemplate() { + return createGroupTemplate(new StringTemplateResourceLoader()); + } + + /** + * 创建WebApp的模板组 {@link GroupTemplate},配置文件使用全局默认
+ * 此时自定义的配置文件可在ClassPath中放入beetl.properties配置 + * + * @return {@link GroupTemplate} + * @since 3.2.0 + */ + public static GroupTemplate createWebAppGroupTemplate() { + return createGroupTemplate(new WebAppResourceLoader()); + } + + /** + * 创建字符串的模板组 {@link GroupTemplate},配置文件使用全局默认
+ * 此时自定义的配置文件可在ClassPath中放入beetl.properties配置 + * + * @param path 相对ClassPath的路径 + * @return {@link GroupTemplate} + * @since 3.2.0 + */ + public static GroupTemplate createClassPathGroupTemplate(String path) { + return createGroupTemplate(new ClasspathResourceLoader(path)); + } + + /** + * 创建文件目录的模板组 {@link GroupTemplate},配置文件使用全局默认,使用UTF-8编码
+ * 此时自定义的配置文件可在ClassPath中放入beetl.properties配置 + * + * @param dir 目录路径(绝对路径) + * @return {@link GroupTemplate} + * @since 3.2.0 + */ + public static GroupTemplate createFileGroupTemplate(String dir) { + return createFileGroupTemplate(dir, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 创建文件目录的模板组 {@link GroupTemplate},配置文件使用全局默认
+ * 此时自定义的配置文件可在ClassPath中放入beetl.properties配置 + * + * @param dir 目录路径(绝对路径) + * @param charset 读取模板文件的编码 + * @return {@link GroupTemplate} + * @since 3.2.0 + */ + public static GroupTemplate createFileGroupTemplate(String dir, Charset charset) { + return createGroupTemplate(new FileResourceLoader(dir, charset.name())); + } + + /** + * 创建自定义的模板组 {@link GroupTemplate},配置文件使用全局默认
+ * 此时自定义的配置文件可在ClassPath中放入beetl.properties配置 + * + * @param loader {@link ResourceLoader},资源加载器 + * @return {@link GroupTemplate} + * @since 3.2.0 + */ + public static GroupTemplate createGroupTemplate(ResourceLoader loader) { + try { + return createGroupTemplate(loader, Configuration.defaultConfiguration()); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 创建自定义的 {@link GroupTemplate} + * + * @param loader {@link ResourceLoader},资源加载器 + * @param conf {@link Configuration} 配置文件 + * @return {@link GroupTemplate} + */ + public static GroupTemplate createGroupTemplate(ResourceLoader loader, Configuration conf) { + return new GroupTemplate(loader, conf); + } + + /** + * 获得模板 + * + * @param groupTemplate {@link GroupTemplate} + * @param source 模板资源,根据不同的 {@link ResourceLoader} 加载不同的模板资源 + * @return {@link Template} + */ + public static Template getTemplate(GroupTemplate groupTemplate, String source) { + return groupTemplate.getTemplate(source); + } + + /** + * 获得字符串模板 + * + * @param source 模板内容 + * @return 模板 + * @since 3.2.0 + */ + public static Template getStrTemplate(String source) { + return getTemplate(createStrGroupTemplate(), source); + } + + /** + * 获得ClassPath模板 + * + * @param path ClassPath路径 + * @param templateFileName 模板内容 + * @return 模板 + * @since 3.2.0 + */ + public static Template getClassPathTemplate(String path, String templateFileName) { + return getTemplate(createClassPathGroupTemplate(path), templateFileName); + } + + /** + * 获得本地文件模板 + * + * @param dir 目录绝对路径 + * @param templateFileName 模板内容 + * @return 模板 + * @since 3.2.0 + */ + public static Template getFileTemplate(String dir, String templateFileName) { + return getTemplate(createFileGroupTemplate(dir), templateFileName); + } + + // ------------------------------------------------------------------------------------- Render + /** + * 渲染模板 + * + * @param template {@link Template} + * @param bindingMap 绑定参数 + * @return 渲染后的内容 + */ + public static String render(Template template, Map bindingMap) { + template.binding(bindingMap); + return template.render(); + } + + /** + * 渲染模板,如果为相对路径,则渲染ClassPath模板,否则渲染本地文件模板 + * + * @param path 路径 + * @param templateFileName 模板文件名 + * @param bindingMap 绑定参数 + * @return 渲染后的内容 + * @since 3.2.0 + */ + public static String render(String path, String templateFileName, Map bindingMap) { + if (FileUtil.isAbsolutePath(path)) { + return render(getFileTemplate(path, templateFileName), bindingMap); + } else { + return render(getClassPathTemplate(path, templateFileName), bindingMap); + } + } + + /** + * 渲染模板 + * + * @param templateContent 模板内容 + * @param bindingMap 绑定参数 + * @return 渲染后的内容 + * @since 3.2.0 + */ + public static String renderFromStr(String templateContent, Map bindingMap) { + return render(getStrTemplate(templateContent), bindingMap); + } + + /** + * 渲染模板 + * + * @param templateContent {@link Template} + * @param bindingMap 绑定参数 + * @param writer {@link Writer} 渲染后写入的目标Writer + * @return {@link Writer} + */ + public static Writer render(Template templateContent, Map bindingMap, Writer writer) { + templateContent.binding(bindingMap); + templateContent.renderTo(writer); + return writer; + } + + /** + * 渲染模板 + * + * @param templateContent 模板内容 + * @param bindingMap 绑定参数 + * @param writer {@link Writer} 渲染后写入的目标Writer + * @return {@link Writer} + */ + public static Writer renderFromStr(String templateContent, Map bindingMap, Writer writer) { + return render(getStrTemplate(templateContent), bindingMap, writer); + } + + /** + * 开始构建 {@link ResourceLoaderBuilder},调用{@link ResourceLoaderBuilder#build()}完成构建 + * + * @return {@link ResourceLoaderBuilder} + */ + public static ResourceLoaderBuilder resourceLoaderBuilder() { + return new ResourceLoaderBuilder(); + } + + /** + * ResourceLoader构建器 + * + * @author Looly + * + */ + public static class ResourceLoaderBuilder { + private CompositeResourceLoader compositeResourceLoader = new CompositeResourceLoader(); + + /** + * 创建 + * + * @return {@link ResourceLoaderBuilder} + */ + public static ResourceLoaderBuilder create() { + return new ResourceLoaderBuilder(); + } + + /** + * 添加一个资源加载器 + * + * @param matcher {@link Matcher} 匹配器 + * @param resourceLoader {@link ResourceLoader} 匹配时对应的资源加载器 + * @return {@link ResourceLoaderBuilder} + */ + public ResourceLoaderBuilder add(Matcher matcher, ResourceLoader resourceLoader) { + compositeResourceLoader.addResourceLoader(matcher, resourceLoader); + return this; + } + + /** + * 构建 + * + * @return {@link ResourceLoader} 资源加载器 + */ + public ResourceLoader build() { + return compositeResourceLoader; + } + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/template/engine/beetl/package-info.java b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/beetl/package-info.java new file mode 100644 index 000000000..44f755a41 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/beetl/package-info.java @@ -0,0 +1,7 @@ +/** + * Beetl实现 + * + * @author looly + * + */ +package cn.hutool.extra.template.engine.beetl; \ No newline at end of file diff --git a/hutool-extra/src/main/java/cn/hutool/extra/template/engine/enjoy/EnjoyEngine.java b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/enjoy/EnjoyEngine.java new file mode 100644 index 000000000..2fc2ca3c9 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/enjoy/EnjoyEngine.java @@ -0,0 +1,96 @@ +package cn.hutool.extra.template.engine.enjoy; + +import org.beetl.core.GroupTemplate; + +import com.jfinal.template.source.FileSourceFactory; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.extra.template.Template; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateConfig.ResourceMode; +import cn.hutool.extra.template.TemplateEngine; + +/** + * Enjoy库的引擎包装 + * + * @author looly + * @since 4.1.10 + */ +public class EnjoyEngine implements TemplateEngine { + + private com.jfinal.template.Engine engine; + private ResourceMode resourceMode; + + // --------------------------------------------------------------------------------- Constructor start + /** + * 默认构造 + */ + public EnjoyEngine() { + this(new TemplateConfig()); + } + + /** + * 构造 + * + * @param config 模板配置 + */ + public EnjoyEngine(TemplateConfig config) { + this(createEngine(config)); + this.resourceMode = config.getResourceMode(); + } + + /** + * 构造 + * + * @param engine {@link com.jfinal.template.Engine} + */ + public EnjoyEngine(com.jfinal.template.Engine engine) { + this.engine = engine; + } + // --------------------------------------------------------------------------------- Constructor end + + @Override + public Template getTemplate(String resource) { + if(ObjectUtil.equal(ResourceMode.STRING, this.resourceMode)) { + return EnjoyTemplate.wrap(this.engine.getTemplateByString(resource)); + } + return EnjoyTemplate.wrap(this.engine.getTemplate(resource)); + } + + /** + * 创建引擎 + * + * @param config 模板配置 + * @return {@link GroupTemplate} + */ + private static com.jfinal.template.Engine createEngine(TemplateConfig config) { + Assert.notNull(config, "Template config is null !"); + final com.jfinal.template.Engine engine = com.jfinal.template.Engine.create("Hutool-Enjoy-Engine-" + IdUtil.fastSimpleUUID()); + engine.setEncoding(config.getCharsetStr()); + + switch (config.getResourceMode()) { + case STRING: + // 默认字符串类型资源: + break; + case CLASSPATH: + engine.setToClassPathSourceFactory(); + engine.setBaseTemplatePath(config.getPath()); + break; + case FILE: + engine.setSourceFactory(new FileSourceFactory()); + engine.setBaseTemplatePath(config.getPath()); + break; + case WEB_ROOT: + engine.setSourceFactory(new FileSourceFactory()); + engine.setBaseTemplatePath(FileUtil.getAbsolutePath(FileUtil.getWebRoot())); + break; + default: + break; + } + + return engine; + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/template/engine/enjoy/EnjoyTemplate.java b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/enjoy/EnjoyTemplate.java new file mode 100644 index 000000000..670733bf0 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/enjoy/EnjoyTemplate.java @@ -0,0 +1,50 @@ +package cn.hutool.extra.template.engine.enjoy; + +import java.io.OutputStream; +import java.io.Serializable; +import java.io.Writer; +import java.util.Map; + +import cn.hutool.extra.template.AbstractTemplate; + +/** + * Engoy模板实现 + * + * @author looly + * @since 4.1.9 + */ +public class EnjoyTemplate extends AbstractTemplate implements Serializable{ + private static final long serialVersionUID = -8157926902932567280L; + + private com.jfinal.template.Template rawTemplate; + + /** + * 包装Enjoy模板 + * + * @param EnjoyTemplate Enjoy的模板对象 {@link com.jfinal.template.Template} + * @return {@link EnjoyTemplate} + */ + public static EnjoyTemplate wrap(com.jfinal.template.Template EnjoyTemplate) { + return (null == EnjoyTemplate) ? null : new EnjoyTemplate(EnjoyTemplate); + } + + /** + * 构造 + * + * @param EnjoyTemplate Enjoy的模板对象 {@link com.jfinal.template.Template} + */ + public EnjoyTemplate(com.jfinal.template.Template EnjoyTemplate) { + this.rawTemplate = EnjoyTemplate; + } + + @Override + public void render(Map bindingMap, Writer writer) { + rawTemplate.render(bindingMap, writer); + } + + @Override + public void render(Map bindingMap, OutputStream out) { + rawTemplate.render(bindingMap, out); + } + +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/template/engine/enjoy/package-info.java b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/enjoy/package-info.java new file mode 100644 index 000000000..b8b477068 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/enjoy/package-info.java @@ -0,0 +1,7 @@ +/** + * Jfinal家的Enjoy模板引擎实现 + * + * @author looly + * + */ +package cn.hutool.extra.template.engine.enjoy; \ No newline at end of file diff --git a/hutool-extra/src/main/java/cn/hutool/extra/template/engine/freemarker/FreemarkerEngine.java b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/freemarker/FreemarkerEngine.java new file mode 100644 index 000000000..738a850de --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/freemarker/FreemarkerEngine.java @@ -0,0 +1,105 @@ +package cn.hutool.extra.template.engine.freemarker; + +import java.io.IOException; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.extra.template.Template; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; +import cn.hutool.extra.template.TemplateException; +import freemarker.cache.ClassTemplateLoader; +import freemarker.cache.FileTemplateLoader; +import freemarker.template.Configuration; + +/** + * Beetl模板引擎封装 + * + * @author looly + */ +public class FreemarkerEngine implements TemplateEngine { + + private Configuration cfg; + + // --------------------------------------------------------------------------------- Constructor start + /** + * 默认构造 + */ + public FreemarkerEngine() { + this(new TemplateConfig()); + } + + /** + * 构造 + * + * @param config 模板配置 + */ + public FreemarkerEngine(TemplateConfig config) { + this(createCfg(config)); + } + + /** + * 构造 + * + * @param freemarkerCfg {@link Configuration} + */ + public FreemarkerEngine(Configuration freemarkerCfg) { + this.cfg = freemarkerCfg; + } + // --------------------------------------------------------------------------------- Constructor end + + @Override + public Template getTemplate(String resource) { + try { + return FreemarkerTemplate.wrap(this.cfg.getTemplate(resource)); + } catch(IOException e) { + throw new IORuntimeException(e); + }catch (Exception e) { + throw new TemplateException(e); + } + } + + /** + * 创建配置项 + * + * @param config 模板配置 + * @return {@link Configuration } + */ + private static Configuration createCfg(TemplateConfig config) { + if (null == config) { + config = new TemplateConfig(); + } + + final Configuration cfg = new Configuration(Configuration.VERSION_2_3_28); + cfg.setLocalizedLookup(false); + cfg.setDefaultEncoding(config.getCharset().toString()); + + switch (config.getResourceMode()) { + case CLASSPATH: + cfg.setTemplateLoader(new ClassTemplateLoader(ClassUtil.getClassLoader(), config.getPath())); + break; + case FILE: + try { + cfg.setTemplateLoader(new FileTemplateLoader(FileUtil.file(config.getPath()))); + } catch (IOException e) { + throw new IORuntimeException(e); + } + break; + case WEB_ROOT: + try { + cfg.setTemplateLoader(new FileTemplateLoader(FileUtil.file(FileUtil.getWebRoot(), config.getPath()))); + } catch (IOException e) { + throw new IORuntimeException(e); + } + break; + case STRING: + cfg.setTemplateLoader(new SimpleStringTemplateLoader()); + break; + default: + break; + } + + return cfg; + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/template/engine/freemarker/FreemarkerTemplate.java b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/freemarker/FreemarkerTemplate.java new file mode 100644 index 000000000..6e6758c3d --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/freemarker/FreemarkerTemplate.java @@ -0,0 +1,59 @@ +package cn.hutool.extra.template.engine.freemarker; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.Serializable; +import java.io.Writer; +import java.util.Map; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.extra.template.AbstractTemplate; +import cn.hutool.extra.template.TemplateException; + +/** + * Freemarker模板实现 + * + * @author looly + */ +public class FreemarkerTemplate extends AbstractTemplate implements Serializable{ + private static final long serialVersionUID = -8157926902932567280L; + + freemarker.template.Template rawTemplate; + + /** + * 包装Freemarker模板 + * + * @param beetlTemplate Beetl的模板对象 {@link freemarker.template.Template} + * @return {@link FreemarkerTemplate} + */ + public static FreemarkerTemplate wrap(freemarker.template.Template beetlTemplate) { + return (null == beetlTemplate) ? null : new FreemarkerTemplate(beetlTemplate); + } + + /** + * 构造 + * + * @param freemarkerTemplate Beetl的模板对象 {@link freemarker.template.Template} + */ + public FreemarkerTemplate(freemarker.template.Template freemarkerTemplate) { + this.rawTemplate = freemarkerTemplate; + } + + @Override + public void render(Map bindingMap, Writer writer) { + try { + rawTemplate.process(bindingMap, writer); + } catch (freemarker.template.TemplateException e) { + throw new TemplateException(e); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + @Override + public void render(Map bindingMap, OutputStream out) { + render(bindingMap, IoUtil.getWriter(out, this.rawTemplate.getEncoding())); + } + +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/template/engine/freemarker/SimpleStringTemplateLoader.java b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/freemarker/SimpleStringTemplateLoader.java new file mode 100644 index 000000000..f1b7f9df1 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/freemarker/SimpleStringTemplateLoader.java @@ -0,0 +1,38 @@ +package cn.hutool.extra.template.engine.freemarker; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; + +import freemarker.cache.TemplateLoader; + +/** + * {@link TemplateLoader} 字符串实现形式
+ * 用于直接获取字符串模板 + * + * @author looly + * @since 4.3.3 + */ +public class SimpleStringTemplateLoader implements TemplateLoader { + + @Override + public Object findTemplateSource(String name) throws IOException { + return name; + } + + @Override + public long getLastModified(Object templateSource) { + return System.currentTimeMillis(); + } + + @Override + public Reader getReader(Object templateSource, String encoding) throws IOException { + return new StringReader((String) templateSource); + } + + @Override + public void closeTemplateSource(Object templateSource) throws IOException { + // ignore + } + +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/template/engine/freemarker/package-info.java b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/freemarker/package-info.java new file mode 100644 index 000000000..f4ac9bb43 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/freemarker/package-info.java @@ -0,0 +1,7 @@ +/** + * Freemarker实现 + * + * @author looly + * + */ +package cn.hutool.extra.template.engine.freemarker; \ No newline at end of file diff --git a/hutool-extra/src/main/java/cn/hutool/extra/template/engine/package-info.java b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/package-info.java new file mode 100644 index 000000000..3b82dc5ec --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/package-info.java @@ -0,0 +1,7 @@ +/** + * 第三方模板引擎实现 + * + * @author looly + * + */ +package cn.hutool.extra.template.engine; \ No newline at end of file diff --git a/hutool-extra/src/main/java/cn/hutool/extra/template/engine/rythm/RythmEngine.java b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/rythm/RythmEngine.java new file mode 100644 index 000000000..e615cd4b5 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/rythm/RythmEngine.java @@ -0,0 +1,72 @@ +package cn.hutool.extra.template.engine.rythm; + +import java.util.Properties; + +import cn.hutool.extra.template.Template; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; + +/** + * Rythm模板引擎
+ * 文档:http://rythmengine.org/doc/index + * + * @author looly + * + */ +public class RythmEngine implements TemplateEngine { + + org.rythmengine.RythmEngine engine; + + // --------------------------------------------------------------------------------- Constructor start + /** + * 默认构造 + */ + public RythmEngine() { + this(new TemplateConfig()); + } + + /** + * 构造 + * + * @param config 模板配置 + */ + public RythmEngine(TemplateConfig config) { + this(createEngine(config)); + } + + /** + * 构造 + * + * @param engine {@link org.rythmengine.RythmEngine} + */ + public RythmEngine(org.rythmengine.RythmEngine engine) { + this.engine = engine; + } + // --------------------------------------------------------------------------------- Constructor end + + @Override + public Template getTemplate(String resource) { + return RythmTemplate.wrap(engine.getTemplate(resource)); + } + + /** + * 创建引擎 + * + * @param config 模板配置 + * @return {@link org.rythmengine.RythmEngine} + */ + private static org.rythmengine.RythmEngine createEngine(TemplateConfig config) { + if (null == config) { + config = new TemplateConfig(); + } + + final Properties props = new Properties(); + final String path = config.getPath(); + if (null != path) { + props.put("home.template", path); + } + + final org.rythmengine.RythmEngine engine = new org.rythmengine.RythmEngine(props); + return engine; + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/template/engine/rythm/RythmTemplate.java b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/rythm/RythmTemplate.java new file mode 100644 index 000000000..bb7706000 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/rythm/RythmTemplate.java @@ -0,0 +1,55 @@ +package cn.hutool.extra.template.engine.rythm; + +import java.io.OutputStream; +import java.io.Serializable; +import java.io.Writer; +import java.util.Map; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.TypeReference; +import cn.hutool.extra.template.AbstractTemplate; +import cn.hutool.extra.template.engine.beetl.BeetlTemplate; + +/** + * Rythm模板包装 + * + * @author looly + * + */ +public class RythmTemplate extends AbstractTemplate implements Serializable { + private static final long serialVersionUID = -132774960373894911L; + + private org.rythmengine.template.ITemplate rawTemplate; + + /** + * 包装Rythm模板 + * + * @param template Rythm的模板对象 {@link org.rythmengine.template.ITemplate} + * @return {@link BeetlTemplate} + */ + public static RythmTemplate wrap(org.rythmengine.template.ITemplate template) { + return (null == template) ? null : new RythmTemplate(template); + } + + /** + * 构造 + * + * @param rawTemplate Velocity模板对象 + */ + public RythmTemplate(org.rythmengine.template.ITemplate rawTemplate) { + this.rawTemplate = rawTemplate; + } + + @Override + public void render(Map bindingMap, Writer writer) { + final Map map = Convert.convert(new TypeReference>() {}, bindingMap); + rawTemplate.__setRenderArgs(map); + rawTemplate.render(writer); + } + + @Override + public void render(Map bindingMap, OutputStream out) { + rawTemplate.__setRenderArgs(bindingMap); + rawTemplate.render(out); + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/template/engine/rythm/package-info.java b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/rythm/package-info.java new file mode 100644 index 000000000..b0125d756 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/rythm/package-info.java @@ -0,0 +1,7 @@ +/** + * Rythm实现 + * + * @author looly + * + */ +package cn.hutool.extra.template.engine.rythm; \ No newline at end of file diff --git a/hutool-extra/src/main/java/cn/hutool/extra/template/engine/thymeleaf/ThymeleafEngine.java b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/thymeleaf/ThymeleafEngine.java new file mode 100644 index 000000000..4d41acef1 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/thymeleaf/ThymeleafEngine.java @@ -0,0 +1,109 @@ +package cn.hutool.extra.template.engine.thymeleaf; + +import org.thymeleaf.templatemode.TemplateMode; +import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver; +import org.thymeleaf.templateresolver.DefaultTemplateResolver; +import org.thymeleaf.templateresolver.FileTemplateResolver; +import org.thymeleaf.templateresolver.ITemplateResolver; +import org.thymeleaf.templateresolver.StringTemplateResolver; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.template.Template; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; + +/** + * Thymeleaf模板引擎实现 + * + * @author looly + * @since 4.1.11 + */ +public class ThymeleafEngine implements TemplateEngine { + + org.thymeleaf.TemplateEngine engine; + TemplateConfig config; + + // --------------------------------------------------------------------------------- Constructor start + /** + * 默认构造 + */ + public ThymeleafEngine() { + this(new TemplateConfig()); + } + + /** + * 构造 + * + * @param config 模板配置 + */ + public ThymeleafEngine(TemplateConfig config) { + this(createEngine(config)); + this.config = config; + } + + /** + * 构造 + * + * @param engine {@link org.thymeleaf.TemplateEngine} + */ + public ThymeleafEngine(org.thymeleaf.TemplateEngine engine) { + this.engine = engine; + } + // --------------------------------------------------------------------------------- Constructor end + + @Override + public Template getTemplate(String resource) { + return ThymeleafTemplate.wrap(this.engine, resource, (null == this.config) ? null : this.config.getCharset()); + } + + /** + * 创建引擎 + * + * @param config 模板配置 + * @return {@link TemplateEngine} + */ + private static org.thymeleaf.TemplateEngine createEngine(TemplateConfig config) { + if (null == config) { + config = new TemplateConfig(); + } + + ITemplateResolver resolver = null; + switch (config.getResourceMode()) { + case CLASSPATH: + final ClassLoaderTemplateResolver classLoaderResolver = new ClassLoaderTemplateResolver(); + classLoaderResolver.setCharacterEncoding(config.getCharsetStr()); + classLoaderResolver.setTemplateMode(TemplateMode.HTML); + classLoaderResolver.setPrefix(StrUtil.addSuffixIfNot(config.getPath(), "/")); + resolver = classLoaderResolver; + break; + case FILE: + final FileTemplateResolver fileResolver = new FileTemplateResolver(); + fileResolver.setCharacterEncoding(config.getCharsetStr()); + fileResolver.setTemplateMode(TemplateMode.HTML); + fileResolver.setPrefix(StrUtil.addSuffixIfNot(config.getPath(), "/")); + resolver = fileResolver; + break; + case WEB_ROOT: + final FileTemplateResolver webRootResolver = new FileTemplateResolver(); + webRootResolver.setCharacterEncoding(config.getCharsetStr()); + webRootResolver.setTemplateMode(TemplateMode.HTML); + webRootResolver.setPrefix(StrUtil.addSuffixIfNot(FileUtil.getAbsolutePath(FileUtil.file(FileUtil.getWebRoot(), config.getPath())), "/")); + resolver = webRootResolver; + break; + case STRING: + resolver = new StringTemplateResolver(); + break; + case COMPOSITE: + resolver = new DefaultTemplateResolver(); + break; + default: + resolver = new DefaultTemplateResolver(); + break; + } + + final org.thymeleaf.TemplateEngine engine = new org.thymeleaf.TemplateEngine(); + engine.setTemplateResolver(resolver); + return engine; + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/template/engine/thymeleaf/ThymeleafTemplate.java b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/thymeleaf/ThymeleafTemplate.java new file mode 100644 index 000000000..a95b9e6b3 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/thymeleaf/ThymeleafTemplate.java @@ -0,0 +1,70 @@ +package cn.hutool.extra.template.engine.thymeleaf; + +import java.io.OutputStream; +import java.io.Serializable; +import java.io.Writer; +import java.nio.charset.Charset; +import java.util.Locale; +import java.util.Map; + +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.Context; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.lang.TypeReference; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.extra.template.AbstractTemplate; + +/** + * Thymeleaf模板实现 + * + * @author looly + * @since 4.1.11 + */ +public class ThymeleafTemplate extends AbstractTemplate implements Serializable { + private static final long serialVersionUID = 781284916568562509L; + + private TemplateEngine engine; + private String template; + private Charset charset; + + /** + * 包装Thymeleaf模板 + * + * @param engine Thymeleaf的模板引擎对象 {@link TemplateEngine} + * @param template 模板路径或模板内容 + * @param charset 编码 + * @return {@link ThymeleafTemplate} + */ + public static ThymeleafTemplate wrap(TemplateEngine engine, String template, Charset charset) { + return (null == engine) ? null : new ThymeleafTemplate(engine, template, charset); + } + + /** + * 构造 + * + * @param engine Thymeleaf的模板对象 {@link TemplateEngine} + * @param template 模板路径或模板内容 + * @param charset 编码 + */ + public ThymeleafTemplate(TemplateEngine engine, String template, Charset charset) { + this.engine = engine; + this.template = template; + this.charset = ObjectUtil.defaultIfNull(charset, CharsetUtil.CHARSET_UTF_8); + } + + @Override + public void render(Map bindingMap, Writer writer) { + final Map map = Convert.convert(new TypeReference>() {}, bindingMap); + final Context context = new Context(Locale.getDefault(), map); + this.engine.process(this.template, context, writer); + } + + @Override + public void render(Map bindingMap, OutputStream out) { + render(bindingMap, IoUtil.getWriter(out, this.charset)); + } + +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/template/engine/thymeleaf/package-info.java b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/thymeleaf/package-info.java new file mode 100644 index 000000000..61fe7168b --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/thymeleaf/package-info.java @@ -0,0 +1,7 @@ +/** + * Thymeleaf实现 + * + * @author looly + * + */ +package cn.hutool.extra.template.engine.thymeleaf; \ No newline at end of file diff --git a/hutool-extra/src/main/java/cn/hutool/extra/template/engine/velocity/SimpleStringResourceLoader.java b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/velocity/SimpleStringResourceLoader.java new file mode 100644 index 000000000..91a65f684 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/velocity/SimpleStringResourceLoader.java @@ -0,0 +1,54 @@ +package cn.hutool.extra.template.engine.velocity; + +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; + +import org.apache.velocity.exception.ResourceNotFoundException; +import org.apache.velocity.runtime.resource.Resource; +import org.apache.velocity.runtime.resource.loader.ResourceLoader; +import org.apache.velocity.util.ExtProperties; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.CharsetUtil; + +/** + * {@link ResourceLoader} 的字符串实现形式
+ * 用于直接获取字符串模板 + * + * @author looly + * + */ +public class SimpleStringResourceLoader extends ResourceLoader { + + @Override + public void init(ExtProperties configuration) { + } + + /** + * 获取资源流 + * + * @param source 字符串模板 + * @return 流 + * @throws ResourceNotFoundException 资源未找到 + */ + public InputStream getResourceStream(String source) throws ResourceNotFoundException { + return IoUtil.toStream(source, CharsetUtil.CHARSET_UTF_8); + } + + @Override + public Reader getResourceReader(String source, String encoding) throws ResourceNotFoundException { + return new StringReader(source); + } + + @Override + public boolean isSourceModified(Resource resource) { + return false; + } + + @Override + public long getLastModified(Resource resource) { + return 0; + } + +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/template/engine/velocity/VelocityEngine.java b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/velocity/VelocityEngine.java new file mode 100644 index 000000000..b2004bc0b --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/velocity/VelocityEngine.java @@ -0,0 +1,108 @@ +package cn.hutool.extra.template.engine.velocity; + +import org.apache.velocity.app.Velocity; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.template.Template; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; + +/** + * Velocity模板引擎 + * + * @author looly + * + */ +public class VelocityEngine implements TemplateEngine { + + private org.apache.velocity.app.VelocityEngine engine; + + // --------------------------------------------------------------------------------- Constructor start + /** + * 默认构造 + */ + public VelocityEngine() { + this(new TemplateConfig()); + } + + /** + * 构造 + * + * @param config 模板配置 + */ + public VelocityEngine(TemplateConfig config) { + this(createEngine(config)); + } + + /** + * 构造 + * + * @param engine {@link org.apache.velocity.app.VelocityEngine} + */ + public VelocityEngine(org.apache.velocity.app.VelocityEngine engine) { + this.engine = engine; + } + // --------------------------------------------------------------------------------- Constructor end + + /** + * 获取原始的引擎对象 + * + * @return 原始引擎对象 + * @since 4.3.0 + */ + public org.apache.velocity.app.VelocityEngine getRowEngine() { + return this.engine; + } + + @Override + public Template getTemplate(String resource) { + return VelocityTemplate.wrap(engine.getTemplate(resource)); + } + + /** + * 创建引擎 + * + * @param config 模板配置 + * @return {@link org.apache.velocity.app.VelocityEngine} + */ + private static org.apache.velocity.app.VelocityEngine createEngine(TemplateConfig config) { + if (null == config) { + config = new TemplateConfig(); + } + + final org.apache.velocity.app.VelocityEngine ve = new org.apache.velocity.app.VelocityEngine(); + // 编码 + final String charsetStr = config.getCharset().toString(); + ve.setProperty(Velocity.INPUT_ENCODING, charsetStr); + // ve.setProperty(Velocity.OUTPUT_ENCODING, charsetStr); + ve.setProperty(Velocity.FILE_RESOURCE_LOADER_CACHE, true); // 使用缓存 + + // loader + switch (config.getResourceMode()) { + case CLASSPATH: + ve.setProperty("file.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader"); + break; + case FILE: + // path + final String path = config.getPath(); + if (null != path) { + ve.setProperty(Velocity.FILE_RESOURCE_LOADER_PATH, path); + } + break; + case WEB_ROOT: + ve.setProperty(Velocity.RESOURCE_LOADER, "webapp"); + ve.setProperty("webapp.resource.loader.class", "org.apache.velocity.tools.view.servlet.WebappLoader"); + ve.setProperty("webapp.resource.loader.path", StrUtil.nullToDefault(config.getPath(), StrUtil.SLASH)); + break; + case STRING: + ve.setProperty(Velocity.RESOURCE_LOADER, "str"); + ve.setProperty("str.resource.loader.class", SimpleStringResourceLoader.class.getName()); + break; + default: + break; + } + + ve.init(); + return ve; + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/template/engine/velocity/VelocityTemplate.java b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/velocity/VelocityTemplate.java new file mode 100644 index 000000000..9b4d61f6d --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/velocity/VelocityTemplate.java @@ -0,0 +1,81 @@ +package cn.hutool.extra.template.engine.velocity; + +import java.io.OutputStream; +import java.io.Serializable; +import java.io.Writer; +import java.util.Map; + +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.Velocity; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.lang.TypeReference; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.template.AbstractTemplate; + +/** + * Velocity模板包装 + * + * @author looly + * + */ +public class VelocityTemplate extends AbstractTemplate implements Serializable { + private static final long serialVersionUID = -132774960373894911L; + + private org.apache.velocity.Template rawTemplate; + private String charset; + + /** + * 包装Velocity模板 + * + * @param template Velocity的模板对象 {@link org.apache.velocity.Template} + * @return {@link VelocityTemplate} + */ + public static VelocityTemplate wrap(org.apache.velocity.Template template) { + return (null == template) ? null : new VelocityTemplate(template); + } + + /** + * 构造 + * + * @param rawTemplate Velocity模板对象 + */ + public VelocityTemplate(org.apache.velocity.Template rawTemplate) { + this.rawTemplate = rawTemplate; + } + + @Override + public void render(Map bindingMap, Writer writer) { + rawTemplate.merge(toContext(bindingMap), writer); + IoUtil.flush(writer); + } + + @Override + public void render(Map bindingMap, OutputStream out) { + if(null == charset) { + loadEncoding(); + } + render(bindingMap, IoUtil.getWriter(out, this.charset)); + } + + /** + * 将Map转为VelocityContext + * + * @param bindingMap 参数绑定的Map + * @return {@link VelocityContext} + */ + private VelocityContext toContext(Map bindingMap) { + final Map map = Convert.convert(new TypeReference>() {}, bindingMap); + return new VelocityContext(map); + } + + /** + * 加载可用的Velocity中预定义的编码 + */ + private void loadEncoding() { + final String charset = (String) Velocity.getProperty(Velocity.INPUT_ENCODING); + this.charset = StrUtil.isEmpty(charset) ? CharsetUtil.UTF_8 : charset; + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/template/engine/velocity/VelocityUtil.java b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/velocity/VelocityUtil.java new file mode 100644 index 000000000..7ae4c33c5 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/velocity/VelocityUtil.java @@ -0,0 +1,338 @@ +package cn.hutool.extra.template.engine.velocity; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; + +import org.apache.velocity.Template; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.Velocity; +import org.apache.velocity.app.VelocityEngine; + +import cn.hutool.core.exceptions.NotInitedException; +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.util.IdUtil; + +/** + * Velocity模板引擎工具类
+ * 使用前必须初始化工具类 + * + * @author xiaoleilu + * + */ +public class VelocityUtil { + + /** 是否初始化了默认引擎 */ + private static boolean isInited; + /** 全局上下文,当设定值时,对于每个模板都有效 */ + private static Map globalContext = new HashMap(); + + /** + * 设置Velocity全局上下文
+ * 当设定值时,对于每个模板都有效 + * + * @param name 名 + * @param value 值 + */ + public void putGlobalContext(String name, Object value) { + globalContext.put(name, value); + } + + /** + * 初始化Velocity全局属性 + * + * @param templateDir 模板所在目录,绝对路径 + * @param charset 编码 + */ + synchronized public static void init(String templateDir, String charset) { + Velocity.init(_newInitedProp(templateDir, charset)); + Velocity.setProperty(Velocity.FILE_RESOURCE_LOADER_CACHE, true); // 使用缓存 + + isInited = true; // 标记完成初始化 + } + + /** + * 初始化全局属性 + * + * @param templateDir 模板目录 + * @param charset 字符集编码 + * @param initedGlobalContext 初始的全局上下文 + */ + public static void init(String templateDir, String charset, Map initedGlobalContext) { + globalContext.putAll(initedGlobalContext); + init(templateDir, charset); + } + + /** + * 新建Velocity模板引擎 + * + * @param templateDir 模板所在目录,绝对路径 + * @param charset 编码 + * @return VelocityEngine + */ + public static VelocityEngine newEngine(String templateDir, String charset) { + final VelocityEngine ve = new VelocityEngine(); + + ve.setProperty(Velocity.FILE_RESOURCE_LOADER_CACHE, true); // 使用缓存 + ve.init(_newInitedProp(templateDir, charset)); + + return ve; + } + + /** + * 获得指定模板填充后的内容 + * + * @param templateDir 模板所在目录,绝对路径 + * @param templateFileName 模板名称 + * @param context 上下文(变量值的容器) + * @param charset 字符集 + * @return 模板和内容匹配后的内容 + */ + public static String getContent(String templateDir, String templateFileName, VelocityContext context, String charset) { + // 初始化模板引擎 + final VelocityEngine ve = newEngine(templateDir, charset); + + return getContent(ve, templateFileName, context); + } + + /** + * 获得指定模板填充后的内容 + * + * @param ve 模板引擎 + * @param templateFileName 模板名称 + * @param context 上下文(变量值的容器) + * @return 模板和内容匹配后的内容 + */ + public static String getContent(VelocityEngine ve, String templateFileName, VelocityContext context) { + final StringWriter writer = new StringWriter(); // StringWriter不需要关闭 + toWriter(ve, templateFileName, context, writer); + return writer.toString(); + } + + /** + * 获得指定模板填充后的内容,使用默认引擎 + * + * @param templateFileName 模板文件 + * @param context 上下文(变量值的容器) + * @return 模板和内容匹配后的内容 + */ + public static String getContent(String templateFileName, VelocityContext context) { + final StringWriter writer = new StringWriter(); // StringWriter不需要关闭 + toWriter(templateFileName, context, writer); + return writer.toString(); + } + + /** + * 生成文件 + * + * @param ve 模板引擎 + * @param templateFileName 模板文件名 + * @param context 上下文 + * @param destPath 目标路径(绝对) + */ + public static void toFile(VelocityEngine ve, String templateFileName, VelocityContext context, String destPath) { + toFile(ve.getTemplate(templateFileName), context, destPath); + } + + /** + * 生成文件,使用默认引擎 + * + * @param templateFileName 模板文件名 + * @param context 模板上下文 + * @param destPath 目标路径(绝对) + */ + public static void toFile(String templateFileName, VelocityContext context, String destPath) { + assertInit(); + + toFile(Velocity.getTemplate(templateFileName), context, destPath); + } + + /** + * 生成文件 + * + * @param template 模板 + * @param context 模板上下文 + * @param destPath 目标路径(绝对) + */ + public static void toFile(Template template, VelocityContext context, String destPath) { + PrintWriter writer = null; + try { + writer = FileUtil.getPrintWriter(destPath, Velocity.getProperty(Velocity.INPUT_ENCODING).toString(), false); + merge(template, context, writer); + } catch (IORuntimeException e) { + throw new UtilException(e, "Write Velocity content to [{}] error!", destPath); + } finally { + IoUtil.close(writer); + } + } + + /** + * 生成内容写入流
+ * 会自动关闭Writer + * + * @param ve 引擎 + * @param templateFileName 模板文件名 + * @param context 上下文 + * @param writer 流 + */ + public static void toWriter(VelocityEngine ve, String templateFileName, VelocityContext context, Writer writer) { + final Template template = ve.getTemplate(templateFileName); + merge(template, context, writer); + } + + /** + * 生成内容写入流
+ * 会自动关闭Writer + * + * @param templateFileName 模板文件名 + * @param context 上下文 + * @param writer 流 + */ + public static void toWriter(String templateFileName, VelocityContext context, Writer writer) { + assertInit(); + + final Template template = Velocity.getTemplate(templateFileName); + merge(template, context, writer); + } + + /** + * 生成内容写到响应内容中
+ * 模板的变量来自于Request的Attribute对象 + * + * @param templateFileName 模板文件 + * @param request 请求对象,用于获取模板中的变量值 + * @param response 响应对象 + */ + public static void toWriter(String templateFileName, javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) { + final VelocityContext context = new VelocityContext(); + parseRequest(context, request); + parseSession(context, request.getSession(false)); + + Writer writer = null; + try { + writer = response.getWriter(); + toWriter(templateFileName, context, writer); + } catch (Exception e) { + throw new UtilException(e, "Write Velocity content template by [{}] to response error!", templateFileName); + } finally { + IoUtil.close(writer); + } + } + + /** + * 融合模板和内容 + * + * @param templateContent 模板的内容字符串 + * @param context 上下文 + * @return 模板和内容匹配后的内容 + */ + public static String merge(String templateContent, VelocityContext context) { + final StringWriter writer = new StringWriter(); + try { + Velocity.evaluate(context, writer, IdUtil.randomUUID(), templateContent); + } catch (Exception e) { + throw new UtilException(e); + } + return writer.toString(); + } + + /** + * 融合模板和内容并写入到Writer + * + * @param template 模板 + * @param context 内容 + * @param writer Writer + */ + public static void merge(Template template, VelocityContext context, Writer writer) { + if (template == null) { + throw new UtilException("Template is null"); + } + if (context == null) { + context = new VelocityContext(globalContext); + } else { + // 加入全局上下文 + for (Entry entry : globalContext.entrySet()) { + context.put(entry.getKey(), entry.getValue()); + } + } + + template.merge(context, writer); + } + + /** + * 将Request中的数据转换为模板引擎
+ * 取值包括Session和Request + * + * @param context 内容 + * @param request 请求对象 + * @return VelocityContext + */ + public static VelocityContext parseRequest(VelocityContext context, javax.servlet.http.HttpServletRequest request) { + final Enumeration attrs = request.getAttributeNames(); + if (attrs != null) { + String attrName = null; + while (attrs.hasMoreElements()) { + attrName = attrs.nextElement(); + context.put(attrName, request.getAttribute(attrName)); + } + } + return context; + } + + /** + * 将Session中的值放入模板上下文 + * + * @param context 模板上下文 + * @param session Session + * @return VelocityContext + */ + public static VelocityContext parseSession(VelocityContext context, javax.servlet.http.HttpSession session) { + if (null != session) { + final Enumeration sessionAttrs = session.getAttributeNames(); + if (sessionAttrs != null) { + String attrName = null; + while (sessionAttrs.hasMoreElements()) { + attrName = sessionAttrs.nextElement(); + context.put(attrName, session.getAttribute(attrName)); + } + } + } + return context; + } + + // -------------------------------------------------------------------------- Private method start + /** + * 新建一个初始化后的属性对象 + * + * @param templateDir 模板所在目录 + * @return 初始化后的属性对象 + */ + private static Properties _newInitedProp(String templateDir, String charset) { + final Properties properties = new Properties(); + + properties.setProperty(Velocity.FILE_RESOURCE_LOADER_PATH, templateDir); + properties.setProperty(Velocity.ENCODING_DEFAULT, charset); + properties.setProperty(Velocity.INPUT_ENCODING, charset); + // properties.setProperty(Velocity.OUTPUT_ENCODING, charset); + + return properties; + } + + /** + * 断言是否初始化默认引擎,若未初始化抛出 异常 + */ + private static void assertInit() { + if (false == isInited) { + throw new NotInitedException("Please use VelocityUtil.init() method to init Velocity default engine!"); + } + } + // -------------------------------------------------------------------------- Private method end +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/template/engine/velocity/package-info.java b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/velocity/package-info.java new file mode 100644 index 000000000..933ed31da --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/template/engine/velocity/package-info.java @@ -0,0 +1,7 @@ +/** + * Velocity实现 + * + * @author looly + * + */ +package cn.hutool.extra.template.engine.velocity; \ No newline at end of file diff --git a/hutool-extra/src/main/java/cn/hutool/extra/template/package-info.java b/hutool-extra/src/main/java/cn/hutool/extra/template/package-info.java new file mode 100644 index 000000000..1be84ba26 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/template/package-info.java @@ -0,0 +1,7 @@ +/** + * 第三方模板引擎封装,提供统一的接口用于适配第三方模板引擎 + * + * @author looly + * + */ +package cn.hutool.extra.template; \ No newline at end of file diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/AbstractResult.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/AbstractResult.java new file mode 100644 index 000000000..5d01acfc9 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/AbstractResult.java @@ -0,0 +1,56 @@ +package cn.hutool.extra.tokenizer; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * 对于未实现{@link Iterator}接口的普通结果类,装饰为{@link Result}
+ * 普通的结果类只需实现{@link #nextWord()} 即可 + * + * @author looly + * + */ +public abstract class AbstractResult implements Result{ + + private Word cachedWord; + + @Override + public boolean hasNext() { + if (this.cachedWord != null) { + return true; + } + + final Word next = nextWord(); + if(null != next) { + this.cachedWord = next; + return true; + } + return false; + } + + /** + * 下一个单词,通过实现此方法获取下一个单词,null表示无下一个结果。 + * @return 下一个单词或null + */ + protected abstract Word nextWord(); + + @Override + public Word next() { + if (false == hasNext()) { + throw new NoSuchElementException("No more word !"); + } + final Word currentWord = this.cachedWord; + this.cachedWord = null; + return currentWord; + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Jcseg result not allow to remove !"); + } + + @Override + public Iterator iterator() { + return this; + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/Result.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/Result.java new file mode 100644 index 000000000..833ede411 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/Result.java @@ -0,0 +1,14 @@ +package cn.hutool.extra.tokenizer; + +import java.util.Iterator; + +/** + * 分词结果接口定义
+ * 实现此接口包装分词器的分词结果,通过实现Iterator相应方法获取分词中的单词 + * + * @author looly + * + */ +public interface Result extends Iterator, Iterable{ + +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/TokenizerEngine.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/TokenizerEngine.java new file mode 100644 index 000000000..3d3748040 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/TokenizerEngine.java @@ -0,0 +1,18 @@ +package cn.hutool.extra.tokenizer; + +/** + * 分词引擎接口定义,用户通过实现此接口完成特定分词引擎的适配 + * + * @author looly + * + */ +public interface TokenizerEngine { + + /** + * 文本分词处理接口,通过实现此接口完成分词,产生分词结果 + * + * @param text 需要分词的文本 + * @return {@link Result}分词结果实现 + */ + Result parse(CharSequence text); +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/TokenizerException.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/TokenizerException.java new file mode 100644 index 000000000..d5a001945 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/TokenizerException.java @@ -0,0 +1,33 @@ +package cn.hutool.extra.tokenizer; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 分词异常 + * + * @author Looly + */ +public class TokenizerException extends RuntimeException { + private static final long serialVersionUID = 8074865854534335463L; + + public TokenizerException(Throwable e) { + super(ExceptionUtil.getMessage(e), e); + } + + public TokenizerException(String message) { + super(message); + } + + public TokenizerException(String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params)); + } + + public TokenizerException(String message, Throwable throwable) { + super(message, throwable); + } + + public TokenizerException(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/TokenizerUtil.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/TokenizerUtil.java new file mode 100644 index 000000000..50d3b4e1a --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/TokenizerUtil.java @@ -0,0 +1,21 @@ +package cn.hutool.extra.tokenizer; + +import cn.hutool.extra.tokenizer.engine.TokenizerFactory; + +/** + * 分词工具类 + * + * @author looly + * @since 4.3.3 + */ +public class TokenizerUtil { + + /** + * 根据用户引入的分词引擎jar,自动创建对应的分词引擎对象 + * + * @return {@link TokenizerEngine} + */ + public static TokenizerEngine createEngine() { + return TokenizerFactory.create(); + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/Word.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/Word.java new file mode 100644 index 000000000..050500cbf --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/Word.java @@ -0,0 +1,31 @@ +package cn.hutool.extra.tokenizer; + +/** + * 表示分词中的一个词 + * + * @author looly + * + */ +public interface Word { + + /** + * 获取单词文本 + * + * @return 单词文本 + */ + String getText(); + + /** + * 获取本词的起始位置 + * + * @return 起始位置 + */ + int getStartOffset(); + + /** + * 获取本词的结束位置 + * + * @return 结束位置 + */ + int getEndOffset(); +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/TokenizerFactory.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/TokenizerFactory.java new file mode 100644 index 000000000..5309ee384 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/TokenizerFactory.java @@ -0,0 +1,82 @@ +package cn.hutool.extra.tokenizer.engine; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.tokenizer.TokenizerEngine; +import cn.hutool.extra.tokenizer.TokenizerException; +import cn.hutool.extra.tokenizer.engine.analysis.SmartcnEngine; +import cn.hutool.extra.tokenizer.engine.ansj.AnsjEngine; +import cn.hutool.extra.tokenizer.engine.hanlp.HanLPEngine; +import cn.hutool.extra.tokenizer.engine.ikanalyzer.IKAnalyzerEngine; +import cn.hutool.extra.tokenizer.engine.jcseg.JcsegEngine; +import cn.hutool.extra.tokenizer.engine.jieba.JiebaEngine; +import cn.hutool.extra.tokenizer.engine.mmseg.MmsegEngine; +import cn.hutool.extra.tokenizer.engine.word.WordEngine; +import cn.hutool.log.StaticLog; + +/** + * 简单分词引擎工厂,用于根据用户引入的分词引擎jar,自动创建对应的引擎 + * + * @author looly + * + */ +public class TokenizerFactory { + /** + * 根据用户引入的分词引擎jar,自动创建对应的分词引擎对象 + * + * @return {@link TokenizerEngine} + */ + public static TokenizerEngine create() { + final TokenizerEngine engine = doCreate(); + StaticLog.debug("Use [{}] Tokenizer Engine As Default.", StrUtil.removeSuffix(engine.getClass().getSimpleName(), "Engine")); + return engine; + } + + /** + * 根据用户引入的分词引擎jar,自动创建对应的分词引擎对象 + * + * @return {@link TokenizerEngine} + */ + private static TokenizerEngine doCreate() { + try { + return new AnsjEngine(); + } catch (NoClassDefFoundError e) { + // ignore + } + try { + return new HanLPEngine(); + } catch (NoClassDefFoundError e) { + // ignore + } + try { + return new IKAnalyzerEngine(); + } catch (NoClassDefFoundError e) { + // ignore + } + try { + return new JcsegEngine(); + } catch (NoClassDefFoundError e) { + // ignore + } + try { + return new JiebaEngine(); + } catch (NoClassDefFoundError e) { + // ignore + } + try { + return new MmsegEngine(); + } catch (NoClassDefFoundError e) { + // ignore + } + try { + return new WordEngine(); + } catch (NoClassDefFoundError e) { + // ignore + } + try { + return new SmartcnEngine(); + } catch (NoClassDefFoundError e) { + // ignore + } + throw new TokenizerException("No tokenizer found ! Please add some tokenizer jar to your project !"); + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/analysis/AnalysisEngine.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/analysis/AnalysisEngine.java new file mode 100644 index 000000000..9bdfb478a --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/analysis/AnalysisEngine.java @@ -0,0 +1,45 @@ +package cn.hutool.extra.tokenizer.engine.analysis; + +import java.io.IOException; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.TokenStream; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.tokenizer.Result; +import cn.hutool.extra.tokenizer.TokenizerEngine; +import cn.hutool.extra.tokenizer.TokenizerException; + +/** + * Lucene-analysis分词抽象封装
+ * 项目地址:https://github.com/apache/lucene-solr/tree/master/lucene/analysis + * + * @author looly + * + */ +public class AnalysisEngine implements TokenizerEngine { + + private Analyzer analyzer; + + /** + * 构造 + * + * @param analyzer 分析器{@link Analyzer} + */ + public AnalysisEngine(Analyzer analyzer) { + this.analyzer = analyzer; + } + + @Override + public Result parse(CharSequence text) { + TokenStream stream; + try { + stream = analyzer.tokenStream("text", StrUtil.str(text)); + stream.reset(); + } catch (IOException e) { + throw new TokenizerException(e); + } + return new AnalysisResult(stream); + } + +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/analysis/AnalysisResult.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/analysis/AnalysisResult.java new file mode 100644 index 000000000..0de0bb5a1 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/analysis/AnalysisResult.java @@ -0,0 +1,43 @@ +package cn.hutool.extra.tokenizer.engine.analysis; + +import java.io.IOException; + +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; + +import cn.hutool.extra.tokenizer.AbstractResult; +import cn.hutool.extra.tokenizer.TokenizerException; +import cn.hutool.extra.tokenizer.Word; + +/** + * Lucene-analysis分词抽象结果封装
+ * 项目地址:https://github.com/apache/lucene-solr/tree/master/lucene/analysis + * + * @author looly + * + */ +public class AnalysisResult extends AbstractResult { + + private TokenStream stream; + + /** + * 构造 + * + * @param stream 分词结果 + */ + public AnalysisResult(TokenStream stream) { + this.stream = stream; + } + + @Override + protected Word nextWord() { + try { + if(this.stream.incrementToken()) { + return new AnalysisWord(this.stream.getAttribute(CharTermAttribute.class)); + } + } catch (IOException e) { + throw new TokenizerException(e); + } + return null; + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/analysis/AnalysisWord.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/analysis/AnalysisWord.java new file mode 100644 index 000000000..cb2ef8500 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/analysis/AnalysisWord.java @@ -0,0 +1,53 @@ +package cn.hutool.extra.tokenizer.engine.analysis; + +import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; +import org.apache.lucene.analysis.tokenattributes.OffsetAttribute; +import org.apache.lucene.util.Attribute; + +import cn.hutool.extra.tokenizer.Word; + +/** + * Lucene-analysis分词中的一个单词包装 + * + * @author looly + * + */ +public class AnalysisWord implements Word { + + private Attribute word; + + /** + * 构造 + * + * @param word {@link CharTermAttribute} + */ + public AnalysisWord(CharTermAttribute word) { + this.word = word; + } + + @Override + public String getText() { + return word.toString(); + } + + @Override + public int getStartOffset() { + if(this.word instanceof OffsetAttribute) { + return ((OffsetAttribute)this.word).startOffset(); + } + return -1; + } + + @Override + public int getEndOffset() { + if(this.word instanceof OffsetAttribute) { + return ((OffsetAttribute)this.word).endOffset(); + } + return -1; + } + + @Override + public String toString() { + return getText(); + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/analysis/SmartcnEngine.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/analysis/SmartcnEngine.java new file mode 100644 index 000000000..8af9916a9 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/analysis/SmartcnEngine.java @@ -0,0 +1,21 @@ +package cn.hutool.extra.tokenizer.engine.analysis; + +import org.apache.lucene.analysis.cn.smart.SmartChineseAnalyzer; + +/** + * Lucene-smartcn分词引擎实现
+ * 项目地址:https://github.com/apache/lucene-solr/tree/master/lucene/analysis/smartcn + * + * @author looly + * + */ +public class SmartcnEngine extends AnalysisEngine { + + /** + * 构造 + */ + public SmartcnEngine() { + super(new SmartChineseAnalyzer()); + } + +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/analysis/package-info.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/analysis/package-info.java new file mode 100644 index 000000000..c506d9875 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/analysis/package-info.java @@ -0,0 +1,8 @@ +/** + * Lucene-analysis分词抽象封装
+ * 项目地址:https://github.com/apache/lucene-solr/tree/master/lucene/analysis + * + * @author looly + * + */ +package cn.hutool.extra.tokenizer.engine.analysis; \ No newline at end of file diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ansj/AnsjEngine.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ansj/AnsjEngine.java new file mode 100644 index 000000000..0942b2316 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ansj/AnsjEngine.java @@ -0,0 +1,42 @@ +package cn.hutool.extra.tokenizer.engine.ansj; + +import org.ansj.splitWord.Analysis; +import org.ansj.splitWord.analysis.ToAnalysis; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.tokenizer.TokenizerEngine; +import cn.hutool.extra.tokenizer.Result; + +/** + * Ansj分词引擎实现
+ * 项目地址:https://github.com/NLPchina/ansj_seg + * + * @author looly + * + */ +public class AnsjEngine implements TokenizerEngine { + + private Analysis analysis; + + /** + * 构造 + */ + public AnsjEngine() { + this(new ToAnalysis()); + } + + /** + * 构造 + * + * @param analysis {@link Analysis} + */ + public AnsjEngine(Analysis analysis) { + this.analysis = analysis; + } + + @Override + public Result parse(CharSequence text) { + return new AnsjResult(analysis.parseStr(StrUtil.str(text))); + } + +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ansj/AnsjResult.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ansj/AnsjResult.java new file mode 100644 index 000000000..0484215be --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ansj/AnsjResult.java @@ -0,0 +1,49 @@ +package cn.hutool.extra.tokenizer.engine.ansj; + +import java.util.Iterator; + +import org.ansj.domain.Term; + +import cn.hutool.extra.tokenizer.Result; +import cn.hutool.extra.tokenizer.Word; + +/** + * Ansj分词结果实现
+ * 项目地址:https://github.com/NLPchina/ansj_seg + * + * @author looly + * + */ +public class AnsjResult implements Result{ + + Iterator result; + + /** + * 构造 + * @param ansjResult 分词结果 + */ + public AnsjResult(org.ansj.domain.Result ansjResult) { + this.result = ansjResult.iterator(); + } + + @Override + public boolean hasNext() { + return result.hasNext(); + } + + @Override + public Word next() { + return new AnsjWord(result.next()); + } + + @Override + public void remove() { + result.remove(); + } + + @Override + public Iterator iterator() { + return this; + } + +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ansj/AnsjWord.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ansj/AnsjWord.java new file mode 100644 index 000000000..2d77573d1 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ansj/AnsjWord.java @@ -0,0 +1,44 @@ +package cn.hutool.extra.tokenizer.engine.ansj; + +import org.ansj.domain.Term; + +import cn.hutool.extra.tokenizer.Word; + +/** + * Ansj分词中的一个单词包装 + * + * @author looly + * + */ +public class AnsjWord implements Word { + private Term term; + + /** + * 构造 + * + * @param term {@link Term} + */ + public AnsjWord(Term term) { + this.term = term; + } + + @Override + public String getText() { + return term.getName(); + } + + @Override + public int getStartOffset() { + return this.term.getOffe(); + } + + @Override + public int getEndOffset() { + return getStartOffset() + getText().length(); + } + + @Override + public String toString() { + return getText(); + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ansj/package-info.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ansj/package-info.java new file mode 100644 index 000000000..61743ca2f --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ansj/package-info.java @@ -0,0 +1,8 @@ +/** + * Ansj分词实现
+ * 项目地址:https://github.com/NLPchina/ansj_seg + * + * @author looly + * + */ +package cn.hutool.extra.tokenizer.engine.ansj; \ No newline at end of file diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/hanlp/HanLPEngine.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/hanlp/HanLPEngine.java new file mode 100644 index 000000000..e8ffda26f --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/hanlp/HanLPEngine.java @@ -0,0 +1,43 @@ +package cn.hutool.extra.tokenizer.engine.hanlp; + +import com.hankcs.hanlp.HanLP; +import com.hankcs.hanlp.seg.Segment; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.tokenizer.TokenizerEngine; +import cn.hutool.extra.tokenizer.Result; + +/** + * HanLP分词引擎实现
+ * 项目地址:https://github.com/hankcs/HanLP + * + * @author looly + * + */ +public class HanLPEngine implements TokenizerEngine { + + private Segment seg; + + /** + * 构造 + * + */ + public HanLPEngine() { + this(HanLP.newSegment()); + } + + /** + * 构造 + * + * @param seg {@link Segment} + */ + public HanLPEngine(Segment seg) { + this.seg = seg; + } + + @Override + public Result parse(CharSequence text) { + return new HanLPResult(this.seg.seg(StrUtil.str(text))); + } + +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/hanlp/HanLPResult.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/hanlp/HanLPResult.java new file mode 100644 index 000000000..cd0872263 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/hanlp/HanLPResult.java @@ -0,0 +1,47 @@ +package cn.hutool.extra.tokenizer.engine.hanlp; + +import java.util.Iterator; +import java.util.List; + +import com.hankcs.hanlp.seg.common.Term; + +import cn.hutool.extra.tokenizer.Result; +import cn.hutool.extra.tokenizer.Word; + +/** + * HanLP分词结果实现
+ * 项目地址:https://github.com/hankcs/HanLP + * + * @author looly + * + */ +public class HanLPResult implements Result { + + Iterator result; + + public HanLPResult(List termList) { + this.result = termList.iterator(); + } + + @Override + public boolean hasNext() { + return result.hasNext(); + } + + @Override + public Word next() { + return new HanLPWord(result.next()); + } + + @Override + public void remove() { + result.remove(); + } + + @Override + public Iterator iterator() { + return this; + } + + +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/hanlp/HanLPWord.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/hanlp/HanLPWord.java new file mode 100644 index 000000000..494fa40d9 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/hanlp/HanLPWord.java @@ -0,0 +1,45 @@ +package cn.hutool.extra.tokenizer.engine.hanlp; + +import com.hankcs.hanlp.seg.common.Term; + +import cn.hutool.extra.tokenizer.Word; + +/** + * HanLP分词中的一个单词包装 + * + * @author looly + * + */ +public class HanLPWord implements Word { + + private Term term; + + /** + * 构造 + * + * @param term {@link Term} + */ + public HanLPWord(Term term) { + this.term = term; + } + + @Override + public String getText() { + return term.word; + } + + @Override + public int getStartOffset() { + return this.term.offset; + } + + @Override + public int getEndOffset() { + return getStartOffset() + this.term.length(); + } + + @Override + public String toString() { + return getText(); + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/hanlp/package-info.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/hanlp/package-info.java new file mode 100644 index 000000000..09c1e4ce4 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/hanlp/package-info.java @@ -0,0 +1,8 @@ +/** + * HanLP分词引擎实现
+ * 项目地址:https://github.com/hankcs/HanLP + * + * @author looly + * + */ +package cn.hutool.extra.tokenizer.engine.hanlp; \ No newline at end of file diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ikanalyzer/IKAnalyzerEngine.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ikanalyzer/IKAnalyzerEngine.java new file mode 100644 index 000000000..9645ff662 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ikanalyzer/IKAnalyzerEngine.java @@ -0,0 +1,43 @@ +package cn.hutool.extra.tokenizer.engine.ikanalyzer; + +import org.wltea.analyzer.core.IKSegmenter; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.tokenizer.TokenizerEngine; +import cn.hutool.extra.tokenizer.Result; + +/** + * IKAnalyzer分词引擎实现
+ * 项目地址:https://github.com/yozhao/IKAnalyzer + * + * @author looly + * + */ +public class IKAnalyzerEngine implements TokenizerEngine { + + private IKSegmenter seg; + + /** + * 构造 + * + */ + public IKAnalyzerEngine() { + this(new IKSegmenter(null, true)); + } + + /** + * 构造 + * + * @param seg {@link IKSegmenter} + */ + public IKAnalyzerEngine(IKSegmenter seg) { + this.seg = seg; + } + + @Override + public Result parse(CharSequence text) { + this.seg.reset(StrUtil.getReader(text)); + return new IKAnalyzerResult(this.seg); + } + +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ikanalyzer/IKAnalyzerResult.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ikanalyzer/IKAnalyzerResult.java new file mode 100644 index 000000000..535780184 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ikanalyzer/IKAnalyzerResult.java @@ -0,0 +1,45 @@ +package cn.hutool.extra.tokenizer.engine.ikanalyzer; + +import java.io.IOException; + +import org.wltea.analyzer.core.IKSegmenter; +import org.wltea.analyzer.core.Lexeme; + +import cn.hutool.extra.tokenizer.AbstractResult; +import cn.hutool.extra.tokenizer.TokenizerException; +import cn.hutool.extra.tokenizer.Word; + +/** + * IKAnalyzer分词结果实现
+ * 项目地址:https://github.com/yozhao/IKAnalyzer + * + * @author looly + * + */ +public class IKAnalyzerResult extends AbstractResult { + + private IKSegmenter seg; + + /** + * 构造 + * + * @param seg 分词结果 + */ + public IKAnalyzerResult(IKSegmenter seg) { + this.seg = seg; + } + + @Override + protected Word nextWord() { + Lexeme next = null; + try { + next = this.seg.next(); + } catch (IOException e) { + throw new TokenizerException(e); + } + if (null != next) { + return new IKAnalyzerWord(next); + } + return null; + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ikanalyzer/IKAnalyzerWord.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ikanalyzer/IKAnalyzerWord.java new file mode 100644 index 000000000..1f7016df8 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ikanalyzer/IKAnalyzerWord.java @@ -0,0 +1,45 @@ +package cn.hutool.extra.tokenizer.engine.ikanalyzer; + +import org.wltea.analyzer.core.Lexeme; + +import cn.hutool.extra.tokenizer.Word; + +/** + * IKAnalyzer分词中的一个单词包装 + * + * @author looly + * + */ +public class IKAnalyzerWord implements Word { + + private Lexeme word; + + /** + * 构造 + * + * @param word {@link Lexeme} + */ + public IKAnalyzerWord(Lexeme word) { + this.word = word; + } + + @Override + public String getText() { + return word.getLexemeText(); + } + + @Override + public int getStartOffset() { + return word.getBeginPosition(); + } + + @Override + public int getEndOffset() { + return word.getEndPosition(); + } + + @Override + public String toString() { + return getText(); + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ikanalyzer/package-info.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ikanalyzer/package-info.java new file mode 100644 index 000000000..b580d9ce0 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/ikanalyzer/package-info.java @@ -0,0 +1,8 @@ +/** + * IKAnalyzer分词引擎实现
+ * 项目地址:https://github.com/yozhao/IKAnalyzer + * + * @author looly + * + */ +package cn.hutool.extra.tokenizer.engine.ikanalyzer; \ No newline at end of file diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jcseg/JcsegEngine.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jcseg/JcsegEngine.java new file mode 100644 index 000000000..8705fa3c8 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jcseg/JcsegEngine.java @@ -0,0 +1,67 @@ +package cn.hutool.extra.tokenizer.engine.jcseg; + +import java.io.IOException; +import java.io.StringReader; + +import org.lionsoul.jcseg.tokenizer.core.ADictionary; +import org.lionsoul.jcseg.tokenizer.core.DictionaryFactory; +import org.lionsoul.jcseg.tokenizer.core.ISegment; +import org.lionsoul.jcseg.tokenizer.core.JcsegException; +import org.lionsoul.jcseg.tokenizer.core.JcsegTaskConfig; +import org.lionsoul.jcseg.tokenizer.core.SegmentFactory; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.tokenizer.TokenizerEngine; +import cn.hutool.extra.tokenizer.Result; +import cn.hutool.extra.tokenizer.TokenizerException; + +/** + * Jcseg分词引擎实现
+ * 项目地址:https://gitee.com/lionsoul/jcseg + * + * @author looly + * + */ +public class JcsegEngine implements TokenizerEngine { + + private ISegment segment; + + /** + * 构造 + */ + public JcsegEngine() { + // 创建JcsegTaskConfig分词配置实例,自动查找加载jcseg.properties配置项来初始化 + JcsegTaskConfig config = new JcsegTaskConfig(true); + // 创建默认单例词库实现,并且按照config配置加载词库 + ADictionary dic = DictionaryFactory.createSingletonDictionary(config); + + // 依据给定的ADictionary和JcsegTaskConfig来创建ISegment + try { + this.segment = SegmentFactory.createJcseg(// + JcsegTaskConfig.COMPLEX_MODE, // + new Object[] { config, dic }); + } catch (JcsegException e) { + throw new TokenizerException(e); + } + } + + /** + * 构造 + * + * @param segment {@link ISegment} + */ + public JcsegEngine(ISegment segment) { + this.segment = segment; + } + + @Override + public Result parse(CharSequence text) { + try { + this.segment.reset(new StringReader(StrUtil.str(text))); + } catch (IOException e) { + throw new TokenizerException(e); + } + return new JcsegResult(this.segment); + } + +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jcseg/JcsegResult.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jcseg/JcsegResult.java new file mode 100644 index 000000000..3ee75aca6 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jcseg/JcsegResult.java @@ -0,0 +1,72 @@ +package cn.hutool.extra.tokenizer.engine.jcseg; + +import java.io.IOException; +import java.util.Iterator; +import java.util.NoSuchElementException; + +import org.lionsoul.jcseg.tokenizer.core.ISegment; +import org.lionsoul.jcseg.tokenizer.core.IWord; + +import cn.hutool.extra.tokenizer.Result; +import cn.hutool.extra.tokenizer.TokenizerException; +import cn.hutool.extra.tokenizer.Word; + +/** + * Jcseg分词结果包装
+ * 项目地址:https://gitee.com/lionsoul/jcseg + * + * @author looly + * + */ +public class JcsegResult implements Result{ + + private ISegment result; + private Word cachedWord; + + /** + * 构造 + * @param segment 分词结果 + */ + public JcsegResult(ISegment segment) { + this.result = segment; + } + + @Override + public boolean hasNext() { + if (this.cachedWord != null) { + return true; + } + IWord next = null; + try { + next = this.result.next(); + } catch (IOException e) { + throw new TokenizerException(e); + } + if(null != next) { + this.cachedWord = new JcsegWord(next); + return true; + } + return false; + } + + @Override + public Word next() { + if (false == hasNext()) { + throw new NoSuchElementException("No more word !"); + } + final Word currentWord = this.cachedWord; + this.cachedWord = null; + return currentWord; + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Jcseg result not allow to remove !"); + } + + @Override + public Iterator iterator() { + return this; + } + +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jcseg/JcsegWord.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jcseg/JcsegWord.java new file mode 100644 index 000000000..38e95f0f6 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jcseg/JcsegWord.java @@ -0,0 +1,44 @@ +package cn.hutool.extra.tokenizer.engine.jcseg; + +import org.lionsoul.jcseg.tokenizer.core.IWord; + +import cn.hutool.extra.tokenizer.Word; + +/** + * Jcseg分词中的一个单词包装 + * + * @author looly + * + */ +public class JcsegWord implements Word { + private IWord word; + + /** + * 构造 + * + * @param word {@link IWord} + */ + public JcsegWord(IWord word) { + this.word = word; + } + + @Override + public String getText() { + return word.getValue(); + } + + @Override + public int getStartOffset() { + return word.getPosition(); + } + + @Override + public int getEndOffset() { + return getStartOffset() + word.getLength(); + } + + @Override + public String toString() { + return getText(); + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jcseg/package-info.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jcseg/package-info.java new file mode 100644 index 000000000..b907a690a --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jcseg/package-info.java @@ -0,0 +1,8 @@ +/** + * Jcseg分词引擎实现
+ * 项目地址:https://gitee.com/lionsoul/jcseg + * + * @author looly + * + */ +package cn.hutool.extra.tokenizer.engine.jcseg; \ No newline at end of file diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jieba/JiebaEngine.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jieba/JiebaEngine.java new file mode 100644 index 000000000..6c7897194 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jieba/JiebaEngine.java @@ -0,0 +1,44 @@ +package cn.hutool.extra.tokenizer.engine.jieba; + +import com.huaban.analysis.jieba.JiebaSegmenter; +import com.huaban.analysis.jieba.JiebaSegmenter.SegMode; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.tokenizer.TokenizerEngine; +import cn.hutool.extra.tokenizer.Result; + +/** + * Jieba分词引擎实现
+ * 项目地址:https://github.com/huaban/jieba-analysis + * + * @author looly + * + */ +public class JiebaEngine implements TokenizerEngine { + + private JiebaSegmenter jiebaSegmenter; + private SegMode mode; + + /** + * 构造 + */ + public JiebaEngine() { + this(SegMode.SEARCH); + } + + /** + * 构造 + * + * @param mode 模式{@link SegMode} + */ + public JiebaEngine(SegMode mode) { + this.jiebaSegmenter = new JiebaSegmenter(); + this.mode = mode; + } + + @Override + public Result parse(CharSequence text) { + return new JiebaResult(jiebaSegmenter.process(StrUtil.str(text), mode)); + } + +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jieba/JiebaResult.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jieba/JiebaResult.java new file mode 100644 index 000000000..94ba56076 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jieba/JiebaResult.java @@ -0,0 +1,50 @@ +package cn.hutool.extra.tokenizer.engine.jieba; + +import java.util.Iterator; +import java.util.List; + +import com.huaban.analysis.jieba.SegToken; + +import cn.hutool.extra.tokenizer.Result; +import cn.hutool.extra.tokenizer.Word; + +/** + * Jieba分词结果实现
+ * 项目地址:https://github.com/huaban/jieba-analysis + * + * @author looly + * + */ +public class JiebaResult implements Result{ + + Iterator result; + + /** + * 构造 + * @param segTokenList 分词结果 + */ + public JiebaResult(List segTokenList) { + this.result = segTokenList.iterator(); + } + + @Override + public boolean hasNext() { + return result.hasNext(); + } + + @Override + public Word next() { + return new JiebaWord(result.next()); + } + + @Override + public void remove() { + result.remove(); + } + + @Override + public Iterator iterator() { + return this; + } + +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jieba/JiebaWord.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jieba/JiebaWord.java new file mode 100644 index 000000000..5e26178e6 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jieba/JiebaWord.java @@ -0,0 +1,44 @@ +package cn.hutool.extra.tokenizer.engine.jieba; + +import com.huaban.analysis.jieba.SegToken; + +import cn.hutool.extra.tokenizer.Word; + +/** + * Jieba分词中的一个单词包装 + * + * @author looly + * + */ +public class JiebaWord implements Word { + private SegToken segToken; + + /** + * 构造 + * + * @param segToken {@link SegToken} + */ + public JiebaWord(SegToken segToken) { + this.segToken = segToken; + } + + @Override + public String getText() { + return segToken.word; + } + + @Override + public int getStartOffset() { + return segToken.startOffset; + } + + @Override + public int getEndOffset() { + return segToken.endOffset; + } + + @Override + public String toString() { + return getText(); + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jieba/package-info.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jieba/package-info.java new file mode 100644 index 000000000..8adaa7718 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/jieba/package-info.java @@ -0,0 +1,8 @@ +/** + * Jieba分词引擎实现
+ * 项目地址:https://github.com/huaban/jieba-analysis + * + * @author looly + * + */ +package cn.hutool.extra.tokenizer.engine.jieba; \ No newline at end of file diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/mmseg/MmsegEngine.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/mmseg/MmsegEngine.java new file mode 100644 index 000000000..8f1bcd1a1 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/mmseg/MmsegEngine.java @@ -0,0 +1,48 @@ +package cn.hutool.extra.tokenizer.engine.mmseg; + +import java.io.StringReader; + +import com.chenlb.mmseg4j.ComplexSeg; +import com.chenlb.mmseg4j.Dictionary; +import com.chenlb.mmseg4j.MMSeg; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.tokenizer.TokenizerEngine; +import cn.hutool.extra.tokenizer.Result; + +/** + * mmseg4j分词引擎实现
+ * 项目地址:https://github.com/chenlb/mmseg4j-core + * + * @author looly + * + */ +public class MmsegEngine implements TokenizerEngine { + + private MMSeg mmSeg; + + /** + * 构造 + */ + public MmsegEngine() { + final Dictionary dict = Dictionary.getInstance(); + final ComplexSeg seg = new ComplexSeg(dict); + this.mmSeg = new MMSeg(new StringReader(""), seg); + } + + /** + * 构造 + * + * @param mmSeg 模式{@link MMSeg} + */ + public MmsegEngine(MMSeg mmSeg) { + this.mmSeg = mmSeg; + } + + @Override + public Result parse(CharSequence text) { + this.mmSeg.reset(StrUtil.getReader(text)); + return new MmsegResult(this.mmSeg); + } + +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/mmseg/MmsegResult.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/mmseg/MmsegResult.java new file mode 100644 index 000000000..f8ec08aba --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/mmseg/MmsegResult.java @@ -0,0 +1,44 @@ +package cn.hutool.extra.tokenizer.engine.mmseg; + +import java.io.IOException; + +import com.chenlb.mmseg4j.MMSeg; + +import cn.hutool.extra.tokenizer.AbstractResult; +import cn.hutool.extra.tokenizer.TokenizerException; +import cn.hutool.extra.tokenizer.Word; + +/** + * mmseg4j分词结果实现
+ * 项目地址:https://github.com/chenlb/mmseg4j-core + * + * @author looly + * + */ +public class MmsegResult extends AbstractResult { + + private MMSeg mmSeg; + + /** + * 构造 + * + * @param mmSeg 分词结果 + */ + public MmsegResult(MMSeg mmSeg) { + this.mmSeg = mmSeg; + } + + @Override + protected Word nextWord() { + com.chenlb.mmseg4j.Word next = null; + try { + next = this.mmSeg.next(); + } catch (IOException e) { + throw new TokenizerException(e); + } + if (null != next) { + return new MmsegWord(next); + } + return null; + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/mmseg/MmsegWord.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/mmseg/MmsegWord.java new file mode 100644 index 000000000..1c60ad4f9 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/mmseg/MmsegWord.java @@ -0,0 +1,43 @@ +package cn.hutool.extra.tokenizer.engine.mmseg; + +import cn.hutool.extra.tokenizer.Word; + +/** + * mmseg分词中的一个单词包装 + * + * @author looly + * + */ +public class MmsegWord implements Word { + + private com.chenlb.mmseg4j.Word word; + + /** + * 构造 + * + * @param word {@link com.chenlb.mmseg4j.Word} + */ + public MmsegWord(com.chenlb.mmseg4j.Word word) { + this.word = word; + } + + @Override + public String getText() { + return word.getString(); + } + + @Override + public int getStartOffset() { + return this.word.getStartOffset(); + } + + @Override + public int getEndOffset() { + return this.word.getEndOffset(); + } + + @Override + public String toString() { + return getText(); + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/mmseg/package-info.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/mmseg/package-info.java new file mode 100644 index 000000000..76eaf940a --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/mmseg/package-info.java @@ -0,0 +1,8 @@ +/** + * mmseg4j分词引擎实现
+ * 项目地址:https://github.com/chenlb/mmseg4j-core + * + * @author looly + * + */ +package cn.hutool.extra.tokenizer.engine.mmseg; \ No newline at end of file diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/package-info.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/package-info.java new file mode 100644 index 000000000..64b3fe245 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/package-info.java @@ -0,0 +1,7 @@ +/** + * 第三方分词引擎实现 + * + * @author looly + * + */ +package cn.hutool.extra.tokenizer.engine; \ No newline at end of file diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/word/WordEngine.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/word/WordEngine.java new file mode 100644 index 000000000..8a1823c01 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/word/WordEngine.java @@ -0,0 +1,52 @@ +package cn.hutool.extra.tokenizer.engine.word; + +import org.apdplat.word.segmentation.Segmentation; +import org.apdplat.word.segmentation.SegmentationAlgorithm; +import org.apdplat.word.segmentation.SegmentationFactory; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.tokenizer.Result; +import cn.hutool.extra.tokenizer.TokenizerEngine; + +/** + * Word分词引擎实现
+ * 项目地址:https://github.com/ysc/word + * + * @author looly + * + */ +public class WordEngine implements TokenizerEngine { + + private Segmentation segmentation; + + /** + * 构造 + */ + public WordEngine() { + this(SegmentationAlgorithm.BidirectionalMaximumMatching); + } + + /** + * 构造 + * + * @param algorithm {@link SegmentationAlgorithm}分词算法枚举 + */ + public WordEngine(SegmentationAlgorithm algorithm) { + this(SegmentationFactory.getSegmentation(algorithm)); + } + + /** + * 构造 + * + * @param segmentation {@link Segmentation}分词实现 + */ + public WordEngine(Segmentation segmentation) { + this.segmentation = segmentation; + } + + @Override + public Result parse(CharSequence text) { + return new WordResult(this.segmentation.seg(StrUtil.str(text))); + } + +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/word/WordResult.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/word/WordResult.java new file mode 100644 index 000000000..49f680ea0 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/word/WordResult.java @@ -0,0 +1,49 @@ +package cn.hutool.extra.tokenizer.engine.word; + +import java.util.Iterator; +import java.util.List; + +import cn.hutool.extra.tokenizer.Result; +import cn.hutool.extra.tokenizer.Word; + +/** + * Word分词结果实现
+ * 项目地址:https://github.com/ysc/word + * + * @author looly + * + */ +public class WordResult implements Result{ + + private Iterator wordIter; + + /** + * 构造 + * + * @param result 分词结果 + */ + public WordResult(List result) { + this.wordIter = result.iterator(); + } + + @Override + public boolean hasNext() { + return this.wordIter.hasNext(); + } + + @Override + public Word next() { + return new WordWord(this.wordIter.next()); + } + + @Override + public void remove() { + this.wordIter.remove(); + } + + @Override + public Iterator iterator() { + return this; + } + +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/word/WordWord.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/word/WordWord.java new file mode 100644 index 000000000..91a351992 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/word/WordWord.java @@ -0,0 +1,43 @@ +package cn.hutool.extra.tokenizer.engine.word; + +import cn.hutool.extra.tokenizer.Word; + +/** + * Word分词中的一个单词包装 + * + * @author looly + * + */ +public class WordWord implements Word { + + private org.apdplat.word.segmentation.Word word; + + /** + * 构造 + * + * @param word {@link org.apdplat.word.segmentation.Word} + */ + public WordWord(org.apdplat.word.segmentation.Word word) { + this.word = word; + } + + @Override + public String getText() { + return word.getText(); + } + + @Override + public int getStartOffset() { + return -1; + } + + @Override + public int getEndOffset() { + return -1; + } + + @Override + public String toString() { + return getText(); + } +} diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/word/package-info.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/word/package-info.java new file mode 100644 index 000000000..c32492e77 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/engine/word/package-info.java @@ -0,0 +1,8 @@ +/** + * Word分词引擎实现
+ * 项目地址:https://github.com/ysc/word + * + * @author looly + * + */ +package cn.hutool.extra.tokenizer.engine.word; \ No newline at end of file diff --git a/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/package-info.java b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/package-info.java new file mode 100644 index 000000000..2a3c03458 --- /dev/null +++ b/hutool-extra/src/main/java/cn/hutool/extra/tokenizer/package-info.java @@ -0,0 +1,8 @@ +/** + * 中文分词封装
+ * 通过定义统一接口,适配第三方分词引擎 + * + * @author looly + * + */ +package cn.hutool.extra.tokenizer; \ No newline at end of file diff --git a/hutool-extra/src/test/java/cn/hutool/extra/emoji/EmojiUtilTest.java b/hutool-extra/src/test/java/cn/hutool/extra/emoji/EmojiUtilTest.java new file mode 100644 index 000000000..fe1620c58 --- /dev/null +++ b/hutool-extra/src/test/java/cn/hutool/extra/emoji/EmojiUtilTest.java @@ -0,0 +1,19 @@ +package cn.hutool.extra.emoji; + +import org.junit.Assert; +import org.junit.Test; + +public class EmojiUtilTest { + + @Test + public void toUnicodeTest() { + String emoji = EmojiUtil.toUnicode(":smile:"); + Assert.assertEquals("😄", emoji); + } + + @Test + public void toAliasTest() { + String alias = EmojiUtil.toAlias("😄"); + Assert.assertEquals(":smile:", alias); + } +} diff --git a/hutool-extra/src/test/java/cn/hutool/extra/ftp/FtpTest.java b/hutool-extra/src/test/java/cn/hutool/extra/ftp/FtpTest.java new file mode 100644 index 000000000..a8213a04d --- /dev/null +++ b/hutool-extra/src/test/java/cn/hutool/extra/ftp/FtpTest.java @@ -0,0 +1,62 @@ +package cn.hutool.extra.ftp; + +import java.util.List; + +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.lang.Console; + +public class FtpTest { + + @Test + @Ignore + public void cdTest() { + Ftp ftp = new Ftp("looly.centos"); + + ftp.cd("/file/aaa"); + Console.log(ftp.pwd()); + + IoUtil.close(ftp); + } + + @Test + @Ignore + public void uploadTest() { + Ftp ftp = new Ftp("looly.centos"); + + List ls = ftp.ls("/file"); + Console.log(ls); + + boolean upload = ftp.upload("/file/aaa", FileUtil.file("E:/qrcodeWithLogo.jpg")); + Console.log(upload); + + IoUtil.close(ftp); + } + + @Test + @Ignore + public void reconnectIfTimeoutTest() throws InterruptedException { + Ftp ftp = new Ftp("looly.centos"); + + Console.log("打印pwd: " + ftp.pwd()); + + Console.log("休眠一段时间,然后再次发送pwd命令,抛出异常表明连接超时"); + Thread.sleep(35 * 1000); + + try{ + Console.log("打印pwd: " + ftp.pwd()); + }catch (FtpException e) { + e.printStackTrace(); + } + + Console.log("判断是否超时并重连..."); + ftp.reconnectIfTimeout(); + + Console.log("打印pwd: " + ftp.pwd()); + + IoUtil.close(ftp); + } +} diff --git a/hutool-extra/src/test/java/cn/hutool/extra/mail/MailAccountTest.java b/hutool-extra/src/test/java/cn/hutool/extra/mail/MailAccountTest.java new file mode 100644 index 000000000..4dc3f0941 --- /dev/null +++ b/hutool-extra/src/test/java/cn/hutool/extra/mail/MailAccountTest.java @@ -0,0 +1,21 @@ +package cn.hutool.extra.mail; + +import org.junit.Assert; +import org.junit.Test; + +/** + * 默认邮件帐户设置测试 + * @author looly + * + */ +public class MailAccountTest { + + @Test + public void parseSettingTest() { + MailAccount account = GlobalMailAccount.INSTANCE.getAccount(); + account.getSmtpProps(); + + Assert.assertNotNull(account.getCharset()); + Assert.assertTrue(account.isSslEnable()); + } +} diff --git a/hutool-extra/src/test/java/cn/hutool/extra/mail/MailTest.java b/hutool-extra/src/test/java/cn/hutool/extra/mail/MailTest.java new file mode 100644 index 000000000..8f05eaf87 --- /dev/null +++ b/hutool-extra/src/test/java/cn/hutool/extra/mail/MailTest.java @@ -0,0 +1,59 @@ +package cn.hutool.extra.mail; + +import java.util.Properties; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.io.FileUtil; + +/** + * 邮件发送测试 + * @author looly + * + */ +public class MailTest { + + @Test + @Ignore + public void sendTest() { + MailUtil.send("hutool@foxmail.com", "测试", "

邮件来自Hutool测试

", true, FileUtil.file("d:/测试附件文本.txt")); + } + + @Test + @Ignore + public void sendTest2() { + //附件名长度大于60时的测试 + MailUtil.send("hutool@foxmail.com", "测试", "

邮件来自Hutool测试

", true, FileUtil.file("d:/6-LongLong一阶段平台建设周报2018.3.12-3.16.xlsx")); + } + + @Test + @Ignore + public void sendHtmlTest() { + MailUtil.send("hutool@foxmail.com", "测试", "

邮件来自Hutool测试

", true); + } + + @Test + @Ignore + public void sendByAccountTest() { + MailAccount account = new MailAccount(); + account.setHost("smtp.yeah.net"); + account.setPort(465); + account.setSslEnable(true); + account.setFrom("hutool@yeah.net"); + account.setUser("hutool"); + account.setPass("q1w2e3"); + MailUtil.send(account, "914104645@qq.com", "测试", "

邮件来自Hutool测试

", true); + } + + @Test + public void mailAccountTest() { + MailAccount account = new MailAccount(); + account.setFrom("hutool@yeah.net"); + account.setDebug(true); + account.defaultIfEmpty(); + Properties props = account.getSmtpProps(); + Assert.assertEquals("true", props.getProperty("mail.debug")); + } +} diff --git a/hutool-extra/src/test/java/cn/hutool/extra/qrcode/QrCodeUtilTest.java b/hutool-extra/src/test/java/cn/hutool/extra/qrcode/QrCodeUtilTest.java new file mode 100644 index 000000000..1aaf1ed08 --- /dev/null +++ b/hutool-extra/src/test/java/cn/hutool/extra/qrcode/QrCodeUtilTest.java @@ -0,0 +1,53 @@ +package cn.hutool.extra.qrcode; + +import java.awt.Color; + +import org.junit.Ignore; +import org.junit.Test; + +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.lang.Console; + +/** + * 二维码工具类单元测试 + * + * @author looly + * + */ +public class QrCodeUtilTest { + + @Test + @Ignore + public void generateTest() { + QrCodeUtil.generate("https://hutool.cn/", 300, 300, FileUtil.file("e:/qrcode.jpg")); + } + + @Test + @Ignore + public void generateCustomTest() { + QrConfig config = new QrConfig(); + config.setMargin(3); + config.setForeColor(Color.CYAN.getRGB()); + config.setBackColor(Color.GRAY.getRGB()); + config.setErrorCorrection(ErrorCorrectionLevel.H); + QrCodeUtil.generate("https://hutool.cn/", config, FileUtil.file("e:/qrcodeCustom.jpg")); + } + + @Test + @Ignore + public void generateWithLogoTest() { + QrCodeUtil.generate(// + "http://hutool.cn/", // + QrConfig.create().setImg("e:/pic/face.jpg"), // + FileUtil.file("e:/qrcodeWithLogo.jpg")); + } + + @Test + @Ignore + public void decodeTest() { + String decode = QrCodeUtil.decode(FileUtil.file("e:/pic/qr.png")); + Console.log(decode); + } +} diff --git a/hutool-extra/src/test/java/cn/hutool/extra/ssh/JschUtilTest.java b/hutool-extra/src/test/java/cn/hutool/extra/ssh/JschUtilTest.java new file mode 100644 index 000000000..5c57880c8 --- /dev/null +++ b/hutool-extra/src/test/java/cn/hutool/extra/ssh/JschUtilTest.java @@ -0,0 +1,64 @@ +package cn.hutool.extra.ssh; + +import org.junit.Ignore; +import org.junit.Test; + +import com.jcraft.jsch.Session; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.lang.Console; + +/** + * Jsch工具类单元测试 + * + * @author looly + * + */ +public class JschUtilTest { + + @Test + @Ignore + public void bindPortTest() { + //新建会话,此会话用于ssh连接到跳板机(堡垒机),此处为10.1.1.1:22 + Session session = JschUtil.getSession("looly.centos", 22, "test", "123456"); + // 将堡垒机保护的内网8080端口映射到localhost,我们就可以通过访问http://localhost:8080/访问内网服务了 + JschUtil.bindPort(session, "172.20.12.123", 8080, 8080); + } + + @Test + @Ignore + public void sftpTest() { + Session session = JschUtil.getSession("looly.centos", 22, "root", "123456"); + Sftp sftp = JschUtil.createSftp(session); + sftp.mkDirs("/opt/test/aaa/bbb"); + Console.log("OK"); + } + + @Test + @Ignore + public void reconnectIfTimeoutTest() throws InterruptedException { + Session session = JschUtil.getSession("sunnyserver", 22,"mysftp","liuyang1234"); + Sftp sftp = JschUtil.createSftp(session); + + Console.log("打印pwd: " + sftp.pwd()); + Console.log("cd / : " + sftp.cd("/")); + Console.log("休眠一段时间,查看是否超时"); + Thread.sleep(30 * 1000); + + try{ + // 当连接超时时,isConnected()仍然返回true,pwd命令也能正常返回,因此,利用发送cd命令的返回结果,来判断是否连接超时 + Console.log("isConnected " + sftp.getClient().isConnected()); + Console.log("打印pwd: " + sftp.pwd()); + Console.log("cd / : " + sftp.cd("/")); + }catch (JschRuntimeException e) { + e.printStackTrace(); + } + + Console.log("调用reconnectIfTimeout方法,判断是否超时并重连"); + sftp.reconnectIfTimeout(); + + Console.log("打印pwd: " + sftp.pwd()); + + IoUtil.close(sftp); + } +} diff --git a/hutool-extra/src/test/java/cn/hutool/extra/template/BeetlUtilTest.java b/hutool-extra/src/test/java/cn/hutool/extra/template/BeetlUtilTest.java new file mode 100644 index 000000000..342906c0c --- /dev/null +++ b/hutool-extra/src/test/java/cn/hutool/extra/template/BeetlUtilTest.java @@ -0,0 +1,35 @@ +package cn.hutool.extra.template; + +import java.io.IOException; + +import org.beetl.core.Configuration; +import org.beetl.core.GroupTemplate; +import org.beetl.core.Template; +import org.beetl.core.resource.StringTemplateResourceLoader; +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.lang.Dict; +import cn.hutool.extra.template.engine.beetl.BeetlUtil; + +/** + * BeetlUtil单元测试 + * + * @author looly + * + */ +public class BeetlUtilTest { + + @Test + public void renderStrTest() throws IOException { + GroupTemplate groupTemplate = BeetlUtil.createGroupTemplate(new StringTemplateResourceLoader(), Configuration.defaultConfiguration()); + Template template = BeetlUtil.getTemplate(groupTemplate, "hello,${name}"); + String result = BeetlUtil.render(template, Dict.create().set("name", "hutool")); + + Assert.assertEquals("hello,hutool", result); + + String renderFromStr = BeetlUtil.renderFromStr("hello,${name}", Dict.create().set("name", "hutool")); + Assert.assertEquals("hello,hutool", renderFromStr); + + } +} diff --git a/hutool-extra/src/test/java/cn/hutool/extra/template/TemplateUtilTest.java b/hutool-extra/src/test/java/cn/hutool/extra/template/TemplateUtilTest.java new file mode 100644 index 000000000..aafdcaeab --- /dev/null +++ b/hutool-extra/src/test/java/cn/hutool/extra/template/TemplateUtilTest.java @@ -0,0 +1,138 @@ +package cn.hutool.extra.template; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.lang.Dict; +import cn.hutool.extra.template.TemplateConfig.ResourceMode; +import cn.hutool.extra.template.engine.beetl.BeetlEngine; +import cn.hutool.extra.template.engine.enjoy.EnjoyEngine; +import cn.hutool.extra.template.engine.freemarker.FreemarkerEngine; +import cn.hutool.extra.template.engine.rythm.RythmEngine; +import cn.hutool.extra.template.engine.thymeleaf.ThymeleafEngine; +import cn.hutool.extra.template.engine.velocity.VelocityEngine; + +/** + * 模板引擎单元测试 + * + * @author looly + * + */ +public class TemplateUtilTest { + + @Test + public void createEngineTest() { + // 默认模板引擎,此处为Beetl + TemplateEngine engine = TemplateUtil.createEngine(new TemplateConfig()); + Template template = engine.getTemplate("hello,${name}"); + String result = template.render(Dict.create().set("name", "hutool")); + Assert.assertEquals("hello,hutool", result); + } + + @Test + public void beetlEngineTest() { + // 字符串模板 + TemplateEngine engine = new BeetlEngine(new TemplateConfig("templates")); + Template template = engine.getTemplate("hello,${name}"); + String result = template.render(Dict.create().set("name", "hutool")); + Assert.assertEquals("hello,hutool", result); + + // classpath中获取模板 + engine = new BeetlEngine(new TemplateConfig("templates", ResourceMode.CLASSPATH)); + Template template2 = engine.getTemplate("beetl_test.btl"); + String result2 = template2.render(Dict.create().set("name", "hutool")); + Assert.assertEquals("hello,hutool", result2); + } + + @Test + public void rythmEngineTest() { + // 字符串模板 + TemplateEngine engine = new RythmEngine(new TemplateConfig("templates")); + Template template = engine.getTemplate("hello,@name"); + String result = template.render(Dict.create().set("name", "hutool")); + Assert.assertEquals("hello,hutool", result); + + // classpath中获取模板 + Template template2 = engine.getTemplate("rythm_test.tmpl"); + String result2 = template2.render(Dict.create().set("name", "hutool")); + Assert.assertEquals("hello,hutool", result2); + } + + @Test + public void freemarkerEngineTest() { + // 字符串模板 + TemplateEngine engine = new FreemarkerEngine(new TemplateConfig("templates", ResourceMode.STRING)); + Template template = engine.getTemplate("hello,${name}"); + String result = template.render(Dict.create().set("name", "hutool")); + Assert.assertEquals("hello,hutool", result); + + //ClassPath模板 + engine = new FreemarkerEngine(new TemplateConfig("templates", ResourceMode.CLASSPATH)); + template = engine.getTemplate("freemarker_test.ftl"); + result = template.render(Dict.create().set("name", "hutool")); + Assert.assertEquals("hello,hutool", result); + } + + @Test + public void velocityEngineTest() { + // 字符串模板 + TemplateEngine engine = new VelocityEngine(new TemplateConfig("templates", ResourceMode.STRING)); + Template template = engine.getTemplate("你好,$name"); + String result = template.render(Dict.create().set("name", "hutool")); + Assert.assertEquals("你好,hutool", result); + + //ClassPath模板 + engine = new VelocityEngine(new TemplateConfig("templates", ResourceMode.CLASSPATH)); + template = engine.getTemplate("templates/velocity_test.vtl"); + result = template.render(Dict.create().set("name", "hutool")); + Assert.assertEquals("你好,hutool", result); + + } + + @Test + public void enjoyEngineTest() { + // 字符串模板 + TemplateEngine engine = new EnjoyEngine(new TemplateConfig("templates")); + Template template = engine.getTemplate("#(x + 123)"); + String result = template.render(Dict.create().set("x", 1)); + Assert.assertEquals("124", result); + + //ClassPath模板 + engine = new EnjoyEngine(new TemplateConfig("templates", ResourceMode.CLASSPATH)); + template = engine.getTemplate("enjoy_test.etl"); + result = template.render(Dict.create().set("x", 1)); + Assert.assertEquals("124", result); + } + + @Test + public void thymeleafEngineTest() { + // 字符串模板 + TemplateEngine engine = new ThymeleafEngine(new TemplateConfig("templates")); + Template template = engine.getTemplate("

"); + String result = template.render(Dict.create().set("message", "Hutool")); + Assert.assertEquals("

Hutool

", result); + + //ClassPath模板 + engine = new ThymeleafEngine(new TemplateConfig("templates", ResourceMode.CLASSPATH)); + template = engine.getTemplate("thymeleaf_test.ttl"); + result = template.render(Dict.create().set("message", "Hutool")); + Assert.assertEquals("

Hutool

", result); + } + + @Test + @Ignore + public void renderToFileTest() { + TemplateEngine engine = new BeetlEngine(new TemplateConfig("templates", ResourceMode.CLASSPATH)); + Template template = engine.getTemplate("freemarker_test.ftl"); + + final Map bindingMap = new HashMap<>(); + bindingMap.put("name", "aa"); + File outputFile = new File("e:/test.txt"); + template.render(bindingMap, outputFile); + } +} diff --git a/hutool-extra/src/test/java/cn/hutool/extra/template/ThymeleafTest.java b/hutool-extra/src/test/java/cn/hutool/extra/template/ThymeleafTest.java new file mode 100644 index 000000000..24fbf5fd7 --- /dev/null +++ b/hutool-extra/src/test/java/cn/hutool/extra/template/ThymeleafTest.java @@ -0,0 +1,99 @@ +package cn.hutool.extra.template; + +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; +import org.thymeleaf.context.Context; +import org.thymeleaf.templateresolver.StringTemplateResolver; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.lang.Dict; +import cn.hutool.extra.template.engine.thymeleaf.ThymeleafEngine; + +/** + * Thymeleaf单元测试 + * + * @author looly + * + */ +public class ThymeleafTest { + + @Test + public void thymeleafEngineTest() { + Map map1 = new HashMap<>(); + map1.put("name", "a"); + + Map map2 = new HashMap<>(); + map2.put("name", "b"); + + // 日期测试 + Map map3 = new HashMap<>(); + map3.put("name", DateUtil.parse("2019-01-01")); + + List> list = new ArrayList<>(); + list.add(map1); + list.add(map2); + list.add(map3); + + // 字符串模板 + TemplateEngine engine = new ThymeleafEngine(new TemplateConfig()); + Template template = engine.getTemplate("

"); + String render = template.render(Dict.create().set("list", list)); + Assert.assertEquals("

a

b

2019-01-01 00:00:00

", render); + } + + @Test + public void thymeleafEngineTest2() { + Map map1 = new HashMap<>(); + map1.put("name", "a"); + + Map map2 = new HashMap<>(); + map2.put("name", "b"); + + // 日期测试 + Map map3 = new HashMap<>(); + map3.put("name", DateUtil.parse("2019-01-01")); + + List> list = new ArrayList<>(); + list.add(map1); + list.add(map2); + list.add(map3); + + LinkedHashMap map = new LinkedHashMap<>(); + map.put("list", list); + + hutoolApi(map); + thymeleaf(map); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private static void thymeleaf(Map map) { + org.thymeleaf.TemplateEngine templateEngine = new org.thymeleaf.TemplateEngine(); + StringTemplateResolver stringTemplateResolver = new StringTemplateResolver(); + templateEngine.addTemplateResolver(stringTemplateResolver); + + StringWriter writer = new StringWriter(); + Context context = new Context(Locale.getDefault(), map); + templateEngine.process("

", context, writer); + + Assert.assertEquals("

a

b

2019-01-01 00:00:00

", writer.toString()); + } + + @SuppressWarnings("rawtypes") + private static void hutoolApi(Map map) { + + // 字符串模板 + TemplateEngine engine = new ThymeleafEngine(new TemplateConfig()); + Template template = engine.getTemplate("

"); + // "

" + String render = template.render(map); + Assert.assertEquals("

a

b

2019-01-01 00:00:00

", render); + } +} diff --git a/hutool-extra/src/test/java/cn/hutool/extra/tokenizer/TokenizerUtilTest.java b/hutool-extra/src/test/java/cn/hutool/extra/tokenizer/TokenizerUtilTest.java new file mode 100644 index 000000000..cc752035a --- /dev/null +++ b/hutool-extra/src/test/java/cn/hutool/extra/tokenizer/TokenizerUtilTest.java @@ -0,0 +1,93 @@ +package cn.hutool.extra.tokenizer; + +import java.util.Iterator; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.extra.tokenizer.engine.analysis.SmartcnEngine; +import cn.hutool.extra.tokenizer.engine.hanlp.HanLPEngine; +import cn.hutool.extra.tokenizer.engine.ikanalyzer.IKAnalyzerEngine; +import cn.hutool.extra.tokenizer.engine.jcseg.JcsegEngine; +import cn.hutool.extra.tokenizer.engine.jieba.JiebaEngine; +import cn.hutool.extra.tokenizer.engine.mmseg.MmsegEngine; +import cn.hutool.extra.tokenizer.engine.word.WordEngine; + +/** + * 模板引擎单元测试 + * + * @author looly + * + */ +public class TokenizerUtilTest { + + String text = "这两个方法的区别在于返回值"; + + @Test + public void createEngineTest() { + // 默认分词引擎,此处为Ansj + TokenizerEngine engine = TokenizerUtil.createEngine(); + Result result = engine.parse(text); + checkResult(result); + } + + @Test + public void hanlpTest() { + TokenizerEngine engine = new HanLPEngine(); + Result result = engine.parse(text); + String resultStr = CollUtil.join((Iterator)result, " "); + Assert.assertEquals("这 两 个 方法 的 区别 在于 返回 值", resultStr); + } + + @Test + public void ikAnalyzerTest() { + TokenizerEngine engine = new IKAnalyzerEngine(); + Result result = engine.parse(text); + String resultStr = CollUtil.join((Iterator)result, " "); + Assert.assertEquals("这两个 方法 的 区别 在于 返回值", resultStr); + } + + @Test + public void jcsegTest() { + TokenizerEngine engine = new JcsegEngine(); + Result result = engine.parse(text); + checkResult(result); + } + + @Test + public void jiebaTest() { + TokenizerEngine engine = new JiebaEngine(); + Result result = engine.parse(text); + String resultStr = CollUtil.join((Iterator)result, " "); + Assert.assertEquals("这 两个 方法 的 区别 在于 返回值", resultStr); + } + + @Test + public void mmsegTest() { + TokenizerEngine engine = new MmsegEngine(); + Result result = engine.parse(text); + checkResult(result); + } + + @Test + public void smartcnTest() { + TokenizerEngine engine = new SmartcnEngine(); + Result result = engine.parse(text); + String resultStr = CollUtil.join((Iterator)result, " "); + Assert.assertEquals("这 两 个 方法 的 区别 在于 返回 值", resultStr); + } + + @Test + public void wordTest() { + TokenizerEngine engine = new WordEngine(); + Result result = engine.parse(text); + String resultStr = CollUtil.join((Iterator)result, " "); + Assert.assertEquals("这两个 方法 的 区别 在于 返回值", resultStr); + } + + private void checkResult(Result result) { + String resultStr = CollUtil.join((Iterator)result, " "); + Assert.assertEquals("这 两个 方法 的 区别 在于 返回 值", resultStr); + } +} diff --git a/hutool-extra/src/test/resources/beetl.properties b/hutool-extra/src/test/resources/beetl.properties new file mode 100644 index 000000000..45af7045f --- /dev/null +++ b/hutool-extra/src/test/resources/beetl.properties @@ -0,0 +1,62 @@ +#\u9ed8\u8ba4\u914d\u7f6e +ENGINE=org.beetl.core.engine.FastRuntimeEngine + +# \u6307\u5b9a\u4e86\u5360\u4f4d\u7b26\u53f7\uff0c\u9ed8\u8ba4\u662f${ }\uff0c\u4e5f\u53ef\u4ee5\u6307\u5b9a\u4e3a\u5176\u4ed6\u5360\u4f4d\u7b26\u3002 +DELIMITER_PLACEHOLDER_START=${ +DELIMITER_PLACEHOLDER_END=} + +# \u6307\u5b9a\u4e86\u8bed\u53e5\u7684\u5b9a\u754c\u7b26\u53f7\uff0c\u9ed8\u8ba4\u662f<% %>\uff0c\u4e5f\u53ef\u4ee5\u6307\u5b9a\u4e3a\u5176\u4ed6\u5b9a\u754c\u7b26\u53f7 +DELIMITER_STATEMENT_START=<% +DELIMITER_STATEMENT_END=%> + +# \u6307\u5b9aIO\u8f93\u51fa\u6a21\u5f0f\uff0c\u9ed8\u8ba4\u662fFALSE,\u5373\u901a\u5e38\u7684\u5b57\u7b26\u8f93\u51fa\uff0c\u5728\u8003\u8651\u9ad8\u6027\u80fd\u60c5\u51b5\u4e0b\uff0c\u53ef\u4ee5\u8bbe\u7f6e\u6210true\u3002\u8be6\u7ec6\u8bf7\u53c2\u8003\u9ad8\u7ea7\u7528\u6cd5 +DIRECT_BYTE_OUTPUT = FALSE + +# \u6307\u5b9a\u4e86\u652f\u6301HTML\u6807\u7b7e\uff0c\u4e14\u7b26\u53f7\u4e3a#,\u9ed8\u8ba4\u914d\u7f6e\u4e0b\uff0c\u6a21\u677f\u5f15\u64ce\u8bc6\u522b<#tag >\u8fd9\u6837\u7684\u7c7b\u4f3chtml\u6807\u7b7e\uff0c\u5e76\u80fd\u8c03\u7528\u76f8\u5e94\u7684\u6807\u7b7e\u51fd\u6570\u6216\u8005\u6a21\u677f\u6587\u4ef6\u3002\u4f60\u4e5f\u53ef\u4ee5\u6307\u5b9a\u522b\u7684\u7b26\u53f7\uff0c\u5982bg: \u5219\u8bc6\u522b +# 用户名(注意:如果使用foxmail邮箱,此处user为qq号) +user = hutool +# 密码 +pass = q1w2e3 +#使用 STARTTLS安全连接 +startttlsEnable = true \ No newline at end of file diff --git a/hutool-extra/src/test/resources/example/beetl-example.properties b/hutool-extra/src/test/resources/example/beetl-example.properties new file mode 100644 index 000000000..b3f664e10 --- /dev/null +++ b/hutool-extra/src/test/resources/example/beetl-example.properties @@ -0,0 +1,68 @@ +#--------------------------------------------------------------------------- +# Template for beetl.properties +# see http://ibeetl.com/guide/beetl.html#header-c6267 +#--------------------------------------------------------------------------- + +#\u9ed8\u8ba4\u914d\u7f6e + +#\u5f15\u64ce\u5b9e\u73b0\u7c7b\uff0c\u9ed8\u8ba4\u5373\u53ef +ENGINE=org.beetl.core.engine.FastRuntimeEngine + +#\u5360\u4f4d\u7b26\u53f7\u5f00\u59cb +DELIMITER_PLACEHOLDER_START=${ +#\u5360\u4f4d\u7b26\u53f7\u7ed3\u675f +DELIMITER_PLACEHOLDER_END=} + +#\u8bed\u53e5\u7684\u5b9a\u754c\u7b26\u53f7\u5f00\u59cb +DELIMITER_STATEMENT_START=<% +#\u8bed\u53e5\u7684\u5b9a\u754c\u7b26\u53f7\u7ed3\u675f +DELIMITER_STATEMENT_END=%> + +#IO\u8f93\u51fa\u6a21\u5f0f\uff0c\u9ed8\u8ba4\u662fFALSE,\u5373\u901a\u5e38\u7684\u5b57\u7b26\u8f93\u51fa\uff0c\u5728\u8003\u8651\u9ad8\u6027\u80fd\u60c5\u51b5\u4e0b\uff0c\u53ef\u4ee5\u8bbe\u7f6e\u6210true +DIRECT_BYTE_OUTPUT = FALSE + +#\u652f\u6301HTML\u6807\u7b7e\uff0c\u4e14\u7b26\u53f7\u4e3a#,\u9ed8\u8ba4\u914d\u7f6e\u4e0b\uff0c\u6a21\u677f\u5f15\u64ce\u8bc6\u522b<#tag >\u8fd9\u6837\u7684\u7c7b\u4f3chtml\u6807\u7b7e\uff0c\u5e76\u80fd\u8c03\u7528\u76f8\u5e94\u7684\u6807\u7b7e\u51fd\u6570\u6216\u8005\u6a21\u677f\u6587\u4ef6\u3002\u4f60\u4e5f\u53ef\u4ee5\u6307\u5b9a\u522b\u7684\u7b26\u53f7\uff0c\u5982bg: \u5219\u8bc6\u522b +# 用户名 +user = hutool +# 密码 +pass = q1w2e3 + +# 是否打开调试模式,调试模式会显示与邮件服务器通信过程,默认不开启 +debug = true +# 编码,决定了邮件正文编码和发件人、收件人、抄送人、密送人姓名中的中文编码,默认UTF-8 +charset = UTF-8 + +#使用 STARTTLS安全连接 +startttlsEnable = true +#使用 SSL安全连接 +sslEnable = true +# 指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字 +socketFactoryClass = javax.net.ssl.SSLSocketFactory +# 如果设置为true,未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true +socketFactoryFallback = true +# 指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口456 +socketFactoryPort = 465 + +# 对于超长参数是否切分为多份,默认为false(国内邮箱附件不支持切分的附件名) +splitlongparameters = false + +# SMTP超时时长,单位毫秒,缺省值不超时 +timeout = 0 +# Socket连接超时值,单位毫秒,缺省值不超时 +connectionTimeout = 0 \ No newline at end of file diff --git a/hutool-extra/src/test/resources/example/velocity-example.vm b/hutool-extra/src/test/resources/example/velocity-example.vm new file mode 100644 index 000000000..fd05b6150 --- /dev/null +++ b/hutool-extra/src/test/resources/example/velocity-example.vm @@ -0,0 +1,52 @@ +##这是一行注释,不会输出 +#* + 这是多行注释,不会输出 + 多行注释 +*# +// ---------- 1.变量赋值输出------------ +Welcome $name to Javayou.com! +today is $date. +tdday is $mydae.//未被定义的变量将当成字符串 + +// -----------2.设置变量值,所有变量都以$开头---------------- +#set( $iAmVariable = "good!" ) +Welcome $name to Javayou.com! +today is $date. +$iAmVariable + +//-------------3.if,else判断-------------------------- +#set ($admin = "admin") +#set ($user = "user") +#if ($admin == $user) + Welcome admin! +#else + Welcome user! +#end + +//--------------4.迭代数据List--------------------- +#foreach( $product in $list ) + $product +#end + +// ------------5.迭代数据HashSet----------------- +#foreach($key in $hashVariable.keySet() ) + $key ‘s value: $ hashVariable.get($key) +#end + +//-----------6.迭代数据List Bean($velocityCount为列举序号,默认从1开始,可调整) +#foreach ($s in $listBean) + <$velocityCount> Address: $s.address +#end + +//-------------7.模板嵌套--------------------- +#foreach ($element in $list) + #foreach ($element in $list) + inner:This is ($velocityCount)- $element. + #end +outer:This is ($velocityCount)- $element. +#end + +//-----------8.导入其它文件,多个文件用逗号隔开(非模板静态)-------------- +#include("com/test2/test.txt") +//-----------8.导入其它文件,多个文件用逗号隔开(模板文件)-------------- +#parse("com/test2/test.txt") \ No newline at end of file diff --git a/hutool-extra/src/test/resources/templates/beetl_test.btl b/hutool-extra/src/test/resources/templates/beetl_test.btl new file mode 100644 index 000000000..b66306d53 --- /dev/null +++ b/hutool-extra/src/test/resources/templates/beetl_test.btl @@ -0,0 +1 @@ +hello,${name} \ No newline at end of file diff --git a/hutool-extra/src/test/resources/templates/enjoy_test.etl b/hutool-extra/src/test/resources/templates/enjoy_test.etl new file mode 100644 index 000000000..b2139d587 --- /dev/null +++ b/hutool-extra/src/test/resources/templates/enjoy_test.etl @@ -0,0 +1 @@ +#(x + 123) \ No newline at end of file diff --git a/hutool-extra/src/test/resources/templates/freemarker_test.ftl b/hutool-extra/src/test/resources/templates/freemarker_test.ftl new file mode 100644 index 000000000..b66306d53 --- /dev/null +++ b/hutool-extra/src/test/resources/templates/freemarker_test.ftl @@ -0,0 +1 @@ +hello,${name} \ No newline at end of file diff --git a/hutool-extra/src/test/resources/templates/rythm_test.tmpl b/hutool-extra/src/test/resources/templates/rythm_test.tmpl new file mode 100644 index 000000000..53bfd69a9 --- /dev/null +++ b/hutool-extra/src/test/resources/templates/rythm_test.tmpl @@ -0,0 +1,2 @@ +@args String name +hello,@name \ No newline at end of file diff --git a/hutool-extra/src/test/resources/templates/thymeleaf_test.ttl b/hutool-extra/src/test/resources/templates/thymeleaf_test.ttl new file mode 100644 index 000000000..29c3d573a --- /dev/null +++ b/hutool-extra/src/test/resources/templates/thymeleaf_test.ttl @@ -0,0 +1 @@ +

\ No newline at end of file diff --git a/hutool-extra/src/test/resources/templates/velocity_test.vtl b/hutool-extra/src/test/resources/templates/velocity_test.vtl new file mode 100644 index 000000000..e2b70433d --- /dev/null +++ b/hutool-extra/src/test/resources/templates/velocity_test.vtl @@ -0,0 +1 @@ +你好,$name \ No newline at end of file diff --git a/hutool-http/pom.xml b/hutool-http/pom.xml new file mode 100644 index 000000000..1d95d3fe7 --- /dev/null +++ b/hutool-http/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + + jar + + + cn.hutool + hutool-parent + 4.6.2-SNAPSHOT + + + hutool-http + ${project.artifactId} + Hutool Http客户端 + + + + cn.hutool + hutool-core + ${project.parent.version} + + + cn.hutool + hutool-log + ${project.parent.version} + + + cn.hutool + hutool-json + ${project.parent.version} + + + javax.xml.soap + javax.xml.soap-api + 1.4.0 + provided + + + diff --git a/hutool-http/src/main/java/cn/hutool/http/ContentType.java b/hutool-http/src/main/java/cn/hutool/http/ContentType.java new file mode 100644 index 000000000..c4af2f4f5 --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/ContentType.java @@ -0,0 +1,110 @@ +package cn.hutool.http; + +import java.nio.charset.Charset; + +import cn.hutool.core.util.StrUtil; + +/** + * 常用Content-Type类型枚举 + * + * @author looly + * @since 4.0.11 + */ +public enum ContentType { + + /** 标准表单编码,当action为get时候,浏览器用x-www-form-urlencoded的编码方式把form数据转换成一个字串(name1=value1&name2=value2…)*/ + FORM_URLENCODED("application/x-www-form-urlencoded"), + /** 文件上传编码,浏览器会把整个表单以控件为单位分割,并为每个部分加上Content-Disposition,并加上分割符(boundary) */ + MULTIPART("multipart/form-data"), + /** Rest请求JSON编码 */ + JSON("application/json"), + /** Rest请求XML编码 */ + XML("application/xml"), + /** Rest请求text/xml编码 */ + TEXT_XML("text/xml"); + + private String value; + private ContentType(String value) { + this.value = value; + } + + @Override + public String toString() { + return value; + } + + /** + * 输出Content-Type字符串,附带编码信息 + * @param charset 编码 + * @return Content-Type字符串 + */ + public String toString(Charset charset) { + return build(this.value, charset); + } + + /** + * 是否为默认Content-Type,默认包括null和application/x-www-form-urlencoded + * + * @param contentType 内容类型 + * @return 是否为默认Content-Type + * @since 4.1.5 + */ + public static boolean isDefault(String contentType) { + return null == contentType || isFormUrlEncoed(contentType); + } + + /** + * 是否为application/x-www-form-urlencoded + * + * @param contentType 内容类型 + * @return 是否为application/x-www-form-urlencoded + */ + public static boolean isFormUrlEncoed(String contentType) { + return StrUtil.startWithIgnoreCase(contentType, FORM_URLENCODED.toString()); + } + + /** + * 从请求参数的body中判断请求的Content-Type类型,支持的类型有: + * + *
+	 * 1. application/json
+	 * 1. application/xml
+	 * 
+ * + * @param body 请求参数体 + * @return Content-Type类型,如果无法判断返回null + */ + public static ContentType get(String body) { + ContentType contentType = null; + if (StrUtil.isNotBlank(body)) { + char firstChar = body.charAt(0); + switch (firstChar) { + case '{': + case '[': + // JSON请求体 + contentType = JSON; + break; + case '<': + // XML请求体 + contentType = XML; + break; + + default: + break; + } + } + return contentType; + } + + /** + * 输出Content-Type字符串,附带编码信息 + * + * @param contentType Content-Type类型 + * @param charset 编码 + * @return Content-Type字符串 + * @since 4.5.4 + */ + public static String build(String contentType, Charset charset) { + return StrUtil.format("{};charset={}", contentType, charset.name()); + } +} diff --git a/hutool-http/src/main/java/cn/hutool/http/GlobalHeaders.java b/hutool-http/src/main/java/cn/hutool/http/GlobalHeaders.java new file mode 100644 index 000000000..10f1e88e1 --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/GlobalHeaders.java @@ -0,0 +1,211 @@ +package cn.hutool.http; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 全局头部信息
+ * 所有Http请求将共用此全局头部信息,除非在{@link HttpRequest}中自定义头部信息覆盖之 + * + * @author looly + * + */ +public enum GlobalHeaders { + INSTANCE; + + /** 存储头信息 */ + protected Map> headers = new HashMap>(); + + /** + * 构造 + */ + private GlobalHeaders() { + putDefault(false); + } + + /** + * 加入默认的头部信息 + * + * @param isReset 是否重置所有头部信息(删除自定义保留默认) + * @return this + */ + public GlobalHeaders putDefault(boolean isReset) { + if (isReset) { + this.headers.clear(); + } + + header(Header.ACCEPT, "text/html,application/json,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", true); + header(Header.ACCEPT_ENCODING, "gzip, deflate", true); + header(Header.ACCEPT_LANGUAGE, "zh-CN,zh;q=0.8", true); + // 此Header只有在post请求中有用,因此在HttpRequest的method方法中设置此头信息,此处去掉 + // header(Header.CONTENT_TYPE, ContentType.FORM_URLENCODED.toString(CharsetUtil.CHARSET_UTF_8), true); + header(Header.USER_AGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36 Hutool", true); + return this; + } + + // ---------------------------------------------------------------- Headers start + /** + * 根据name获取头信息 + * + * @param name Header名 + * @return Header值 + */ + public String header(String name) { + final List values = headerList(name); + if (CollectionUtil.isEmpty(values)) { + return null; + } + return values.get(0); + } + + /** + * 根据name获取头信息列表 + * + * @param name Header名 + * @return Header值 + * @since 3.1.1 + */ + public List headerList(String name) { + if (StrUtil.isBlank(name)) { + return null; + } + + return headers.get(name.trim()); + } + + /** + * 根据name获取头信息 + * + * @param name Header名 + * @return Header值 + */ + public String header(Header name) { + if (null == name) { + return null; + } + return header(name.toString()); + } + + /** + * 设置一个header
+ * 如果覆盖模式,则替换之前的值,否则加入到值列表中 + * + * @param name Header名 + * @param value Header值 + * @param isOverride 是否覆盖已有值 + * @return this + */ + public GlobalHeaders header(String name, String value, boolean isOverride) { + if (null != name && null != value) { + final List values = headers.get(name.trim()); + if (isOverride || CollectionUtil.isEmpty(values)) { + final ArrayList valueList = new ArrayList(); + valueList.add(value); + headers.put(name.trim(), valueList); + } else { + values.add(value.trim()); + } + } + return this; + } + + /** + * 设置一个header
+ * 如果覆盖模式,则替换之前的值,否则加入到值列表中 + * + * @param name Header名 + * @param value Header值 + * @param isOverride 是否覆盖已有值 + * @return this + */ + public GlobalHeaders header(Header name, String value, boolean isOverride) { + return header(name.toString(), value, isOverride); + } + + /** + * 设置一个header
+ * 覆盖模式,则替换之前的值 + * + * @param name Header名 + * @param value Header值 + * @return this + */ + public GlobalHeaders header(Header name, String value) { + return header(name.toString(), value, true); + } + + /** + * 设置一个header
+ * 覆盖模式,则替换之前的值 + * + * @param name Header名 + * @param value Header值 + * @return this + */ + public GlobalHeaders header(String name, String value) { + return header(name, value, true); + } + + /** + * 设置请求头
+ * 不覆盖原有请求头 + * + * @param headers 请求头 + * @return this + */ + public GlobalHeaders header(Map> headers) { + if (CollectionUtil.isEmpty(headers)) { + return this; + } + + String name; + for (Entry> entry : headers.entrySet()) { + name = entry.getKey(); + for (String value : entry.getValue()) { + this.header(name, StrUtil.nullToEmpty(value), false); + } + } + return this; + } + + /** + * 移除一个头信息 + * + * @param name Header名 + * @return this + */ + public GlobalHeaders removeHeader(String name) { + if (name != null) { + headers.remove(name.trim()); + } + return this; + } + + /** + * 移除一个头信息 + * + * @param name Header名 + * @return this + */ + public GlobalHeaders removeHeader(Header name) { + return removeHeader(name.toString()); + } + + /** + * 获取headers + * + * @return Headers Map + */ + public Map> headers() { + return Collections.unmodifiableMap(headers); + } + // ---------------------------------------------------------------- Headers end + +} diff --git a/hutool-http/src/main/java/cn/hutool/http/HTMLFilter.java b/hutool-http/src/main/java/cn/hutool/http/HTMLFilter.java new file mode 100644 index 000000000..3b2dc82d2 --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/HTMLFilter.java @@ -0,0 +1,527 @@ +package cn.hutool.http; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import cn.hutool.log.StaticLog; + +/** + * + * HTML filtering utility for protecting against XSS (Cross Site Scripting). + * + * This code is licensed LGPLv3 + * + * This code is a Java port of the original work in PHP by Cal Hendersen. http://code.iamcal.com/php/lib_filter/ + * + * The trickiest part of the translation was handling the differences in regex handling between PHP and Java. These resources were helpful in the process: + * + * http://java.sun.com/j2se/1.4.2/docs/api/java/util/regex/Pattern.html http://us2.php.net/manual/en/reference.pcre.pattern.modifiers.php http://www.regular-expressions.info/modifiers.html + * + * A note on naming conventions: instance variables are prefixed with a "v"; global constants are in all caps. + * + * Sample use: String input = ... String clean = new HTMLFilter().filter( input ); + * + * The class is not thread safe. Create a new instance if in doubt. + * + * If you find bugs or have suggestions on improvement (especially regarding performance), please contact us. The latest version of this source, and our contact details, can be found at + * http://xss-html-filter.sf.net + * + * @author Joseph O'Connell + * @author Cal Hendersen + * @author Michael Semb Wever + */ +public final class HTMLFilter { + + /** regex flag union representing /si modifiers in php **/ + private static final int REGEX_FLAGS_SI = Pattern.CASE_INSENSITIVE | Pattern.DOTALL; + private static final Pattern P_COMMENTS = Pattern.compile("", Pattern.DOTALL); + private static final Pattern P_COMMENT = Pattern.compile("^!--(.*)--$", REGEX_FLAGS_SI); + private static final Pattern P_TAGS = Pattern.compile("<(.*?)>", Pattern.DOTALL); + private static final Pattern P_END_TAG = Pattern.compile("^/([a-z0-9]+)", REGEX_FLAGS_SI); + private static final Pattern P_START_TAG = Pattern.compile("^([a-z0-9]+)(.*?)(/?)$", REGEX_FLAGS_SI); + private static final Pattern P_QUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)=([\"'])(.*?)\\2", REGEX_FLAGS_SI); + private static final Pattern P_UNQUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)(=)([^\"\\s']+)", REGEX_FLAGS_SI); + private static final Pattern P_PROTOCOL = Pattern.compile("^([^:]+):", REGEX_FLAGS_SI); + private static final Pattern P_ENTITY = Pattern.compile("&#(\\d+);?"); + private static final Pattern P_ENTITY_UNICODE = Pattern.compile("&#x([0-9a-f]+);?"); + private static final Pattern P_ENCODE = Pattern.compile("%([0-9a-f]{2});?"); + private static final Pattern P_VALID_ENTITIES = Pattern.compile("&([^&;]*)(?=(;|&|$))"); + private static final Pattern P_VALID_QUOTES = Pattern.compile("(>|^)([^<]+?)(<|$)", Pattern.DOTALL); + private static final Pattern P_END_ARROW = Pattern.compile("^>"); + private static final Pattern P_BODY_TO_END = Pattern.compile("<([^>]*?)(?=<|$)"); + private static final Pattern P_XML_CONTENT = Pattern.compile("(^|>)([^<]*?)(?=>)"); + private static final Pattern P_STRAY_LEFT_ARROW = Pattern.compile("<([^>]*?)(?=<|$)"); + private static final Pattern P_STRAY_RIGHT_ARROW = Pattern.compile("(^|>)([^<]*?)(?=>)"); + private static final Pattern P_AMP = Pattern.compile("&"); + private static final Pattern P_QUOTE = Pattern.compile("\""); + private static final Pattern P_LEFT_ARROW = Pattern.compile("<"); + private static final Pattern P_RIGHT_ARROW = Pattern.compile(">"); + private static final Pattern P_BOTH_ARROWS = Pattern.compile("<>"); + + // @xxx could grow large... maybe use sesat's ReferenceMap + private static final ConcurrentMap P_REMOVE_PAIR_BLANKS = new ConcurrentHashMap(); + private static final ConcurrentMap P_REMOVE_SELF_BLANKS = new ConcurrentHashMap(); + + /** set of allowed html elements, along with allowed attributes for each element **/ + private final Map> vAllowed; + /** counts of open tags for each (allowable) html element **/ + private final Map vTagCounts = new HashMap(); + + /** html elements which must always be self-closing (e.g. "") **/ + private final String[] vSelfClosingTags; + /** html elements which must always have separate opening and closing tags (e.g. "") **/ + private final String[] vNeedClosingTags; + /** set of disallowed html elements **/ + private final String[] vDisallowed; + /** attributes which should be checked for valid protocols **/ + private final String[] vProtocolAtts; + /** allowed protocols **/ + private final String[] vAllowedProtocols; + /** tags which should be removed if they contain no content (e.g. "" or "") **/ + private final String[] vRemoveBlanks; + /** entities allowed within html markup **/ + private final String[] vAllowedEntities; + /** flag determining whether comments are allowed in input String. */ + private final boolean stripComment; + private final boolean encodeQuotes; + private boolean vDebug = false; + /** + * flag determining whether to try to make tags when presented with "unbalanced" angle brackets (e.g. "" becomes " text "). If set to false, unbalanced angle brackets will be + * html escaped. + */ + private final boolean alwaysMakeTags; + + /** + * Default constructor. + * + */ + public HTMLFilter() { + vAllowed = new HashMap>(); + + final ArrayList a_atts = new ArrayList(); + a_atts.add("href"); + a_atts.add("target"); + vAllowed.put("a", a_atts); + + final ArrayList img_atts = new ArrayList(); + img_atts.add("src"); + img_atts.add("width"); + img_atts.add("height"); + img_atts.add("alt"); + vAllowed.put("img", img_atts); + + final ArrayList no_atts = new ArrayList(); + vAllowed.put("b", no_atts); + vAllowed.put("strong", no_atts); + vAllowed.put("i", no_atts); + vAllowed.put("em", no_atts); + + vSelfClosingTags = new String[] { "img" }; + vNeedClosingTags = new String[] { "a", "b", "strong", "i", "em" }; + vDisallowed = new String[] {}; + vAllowedProtocols = new String[] { "http", "mailto", "https" }; // no ftp. + vProtocolAtts = new String[] { "src", "href" }; + vRemoveBlanks = new String[] { "a", "b", "strong", "i", "em" }; + vAllowedEntities = new String[] { "amp", "gt", "lt", "quot" }; + stripComment = true; + encodeQuotes = true; + alwaysMakeTags = true; + } + + /** + * Set debug flag to true. Otherwise use default settings. See the default constructor. + * + * @param debug turn debug on with a true argument + */ + public HTMLFilter(final boolean debug) { + this(); + vDebug = debug; + + } + + /** + * Map-parameter configurable constructor. + * + * @param conf map containing configuration. keys match field names. + */ + @SuppressWarnings("unchecked") + public HTMLFilter(final Map conf) { + + assert conf.containsKey("vAllowed") : "configuration requires vAllowed"; + assert conf.containsKey("vSelfClosingTags") : "configuration requires vSelfClosingTags"; + assert conf.containsKey("vNeedClosingTags") : "configuration requires vNeedClosingTags"; + assert conf.containsKey("vDisallowed") : "configuration requires vDisallowed"; + assert conf.containsKey("vAllowedProtocols") : "configuration requires vAllowedProtocols"; + assert conf.containsKey("vProtocolAtts") : "configuration requires vProtocolAtts"; + assert conf.containsKey("vRemoveBlanks") : "configuration requires vRemoveBlanks"; + assert conf.containsKey("vAllowedEntities") : "configuration requires vAllowedEntities"; + + vAllowed = Collections.unmodifiableMap((HashMap>) conf.get("vAllowed")); + vSelfClosingTags = (String[]) conf.get("vSelfClosingTags"); + vNeedClosingTags = (String[]) conf.get("vNeedClosingTags"); + vDisallowed = (String[]) conf.get("vDisallowed"); + vAllowedProtocols = (String[]) conf.get("vAllowedProtocols"); + vProtocolAtts = (String[]) conf.get("vProtocolAtts"); + vRemoveBlanks = (String[]) conf.get("vRemoveBlanks"); + vAllowedEntities = (String[]) conf.get("vAllowedEntities"); + stripComment = conf.containsKey("stripComment") ? (Boolean) conf.get("stripComment") : true; + encodeQuotes = conf.containsKey("encodeQuotes") ? (Boolean) conf.get("encodeQuotes") : true; + alwaysMakeTags = conf.containsKey("alwaysMakeTags") ? (Boolean) conf.get("alwaysMakeTags") : true; + } + + private void reset() { + vTagCounts.clear(); + } + + private void debug(final String msg) { + if (vDebug) { + StaticLog.debug(msg); + } + } + + // --------------------------------------------------------------- + // my versions of some PHP library functions + public static String chr(final int decimal) { + return String.valueOf((char) decimal); + } + + public static String htmlSpecialChars(final String s) { + String result = s; + result = regexReplace(P_AMP, "&", result); + result = regexReplace(P_QUOTE, """, result); + result = regexReplace(P_LEFT_ARROW, "<", result); + result = regexReplace(P_RIGHT_ARROW, ">", result); + return result; + } + + // --------------------------------------------------------------- + /** + * given a user submitted input String, filter out any invalid or restricted html. + * + * @param input text (i.e. submitted by a user) than may contain html + * @return "clean" version of input, with only valid, whitelisted html elements allowed + */ + public String filter(final String input) { + reset(); + String s = input; + + debug("************************************************"); + debug(" INPUT: " + input); + + s = escapeComments(s); + debug(" escapeComments: " + s); + + s = balanceHTML(s); + debug(" balanceHTML: " + s); + + s = checkTags(s); + debug(" checkTags: " + s); + + s = processRemoveBlanks(s); + debug("processRemoveBlanks: " + s); + + s = validateEntities(s); + debug(" validateEntites: " + s); + + debug("************************************************\n\n"); + return s; + } + + public boolean isAlwaysMakeTags() { + return alwaysMakeTags; + } + + public boolean isStripComments() { + return stripComment; + } + + private String escapeComments(final String s) { + final Matcher m = P_COMMENTS.matcher(s); + final StringBuffer buf = new StringBuffer(); + if (m.find()) { + final String match = m.group(1); // (.*?) + m.appendReplacement(buf, Matcher.quoteReplacement("")); + } + m.appendTail(buf); + + return buf.toString(); + } + + private String balanceHTML(String s) { + if (alwaysMakeTags) { + // + // try and form html + // + s = regexReplace(P_END_ARROW, "", s); + s = regexReplace(P_BODY_TO_END, "<$1>", s); + s = regexReplace(P_XML_CONTENT, "$1<$2", s); + + } else { + // + // escape stray brackets + // + s = regexReplace(P_STRAY_LEFT_ARROW, "<$1", s); + s = regexReplace(P_STRAY_RIGHT_ARROW, "$1$2><", s); + + // + // the last regexp causes '<>' entities to appear + // (we need to do a lookahead assertion so that the last bracket can + // be used in the next pass of the regexp) + // + s = regexReplace(P_BOTH_ARROWS, "", s); + } + + return s; + } + + private String checkTags(String s) { + Matcher m = P_TAGS.matcher(s); + + final StringBuffer buf = new StringBuffer(); + while (m.find()) { + String replaceStr = m.group(1); + replaceStr = processTag(replaceStr); + m.appendReplacement(buf, Matcher.quoteReplacement(replaceStr)); + } + m.appendTail(buf); + + s = buf.toString(); + + // these get tallied in processTag + // (remember to reset before subsequent calls to filter method) + for (String key : vTagCounts.keySet()) { + for (int ii = 0; ii < vTagCounts.get(key); ii++) { + s += ""; + } + } + + return s; + } + + private String processRemoveBlanks(final String s) { + String result = s; + for (String tag : vRemoveBlanks) { + if (!P_REMOVE_PAIR_BLANKS.containsKey(tag)) { + P_REMOVE_PAIR_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?>")); + } + result = regexReplace(P_REMOVE_PAIR_BLANKS.get(tag), "", result); + if (!P_REMOVE_SELF_BLANKS.containsKey(tag)) { + P_REMOVE_SELF_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?/>")); + } + result = regexReplace(P_REMOVE_SELF_BLANKS.get(tag), "", result); + } + + return result; + } + + private static String regexReplace(final Pattern regex_pattern, final String replacement, final String s) { + Matcher m = regex_pattern.matcher(s); + return m.replaceAll(replacement); + } + + private String processTag(final String s) { + // ending tags + Matcher m = P_END_TAG.matcher(s); + if (m.find()) { + final String name = m.group(1).toLowerCase(); + if (allowed(name)) { + if (false == inArray(name, vSelfClosingTags)) { + if (vTagCounts.containsKey(name)) { + vTagCounts.put(name, vTagCounts.get(name) - 1); + return ""; + } + } + } + } + + // starting tags + m = P_START_TAG.matcher(s); + if (m.find()) { + final String name = m.group(1).toLowerCase(); + final String body = m.group(2); + String ending = m.group(3); + + // debug( "in a starting tag, name='" + name + "'; body='" + body + "'; ending='" + ending + "'" ); + if (allowed(name)) { + String params = ""; + + final Matcher m2 = P_QUOTED_ATTRIBUTES.matcher(body); + final Matcher m3 = P_UNQUOTED_ATTRIBUTES.matcher(body); + final List paramNames = new ArrayList(); + final List paramValues = new ArrayList(); + while (m2.find()) { + paramNames.add(m2.group(1)); // ([a-z0-9]+) + paramValues.add(m2.group(3)); // (.*?) + } + while (m3.find()) { + paramNames.add(m3.group(1)); // ([a-z0-9]+) + paramValues.add(m3.group(3)); // ([^\"\\s']+) + } + + String paramName, paramValue; + for (int ii = 0; ii < paramNames.size(); ii++) { + paramName = paramNames.get(ii).toLowerCase(); + paramValue = paramValues.get(ii); + + // debug( "paramName='" + paramName + "'" ); + // debug( "paramValue='" + paramValue + "'" ); + // debug( "allowed? " + vAllowed.get( name ).contains( paramName ) ); + + if (allowedAttribute(name, paramName)) { + if (inArray(paramName, vProtocolAtts)) { + paramValue = processParamProtocol(paramValue); + } + params += " " + paramName + "=\"" + paramValue + "\""; + } + } + + if (inArray(name, vSelfClosingTags)) { + ending = " /"; + } + + if (inArray(name, vNeedClosingTags)) { + ending = ""; + } + + if (ending == null || ending.length() < 1) { + if (vTagCounts.containsKey(name)) { + vTagCounts.put(name, vTagCounts.get(name) + 1); + } else { + vTagCounts.put(name, 1); + } + } else { + ending = " /"; + } + return "<" + name + params + ending + ">"; + } else { + return ""; + } + } + + // comments + m = P_COMMENT.matcher(s); + if (!stripComment && m.find()) { + return "<" + m.group() + ">"; + } + + return ""; + } + + private String processParamProtocol(String s) { + s = decodeEntities(s); + final Matcher m = P_PROTOCOL.matcher(s); + if (m.find()) { + final String protocol = m.group(1); + if (!inArray(protocol, vAllowedProtocols)) { + // bad protocol, turn into local anchor link instead + s = "#" + s.substring(protocol.length() + 1, s.length()); + if (s.startsWith("#//")) { + s = "#" + s.substring(3, s.length()); + } + } + } + + return s; + } + + private String decodeEntities(String s) { + StringBuffer buf = new StringBuffer(); + + Matcher m = P_ENTITY.matcher(s); + while (m.find()) { + final String match = m.group(1); + final int decimal = Integer.decode(match).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + buf = new StringBuffer(); + m = P_ENTITY_UNICODE.matcher(s); + while (m.find()) { + final String match = m.group(1); + final int decimal = Integer.valueOf(match, 16).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + buf = new StringBuffer(); + m = P_ENCODE.matcher(s); + while (m.find()) { + final String match = m.group(1); + final int decimal = Integer.valueOf(match, 16).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + s = validateEntities(s); + return s; + } + + private String validateEntities(final String s) { + StringBuffer buf = new StringBuffer(); + + // validate entities throughout the string + Matcher m = P_VALID_ENTITIES.matcher(s); + while (m.find()) { + final String one = m.group(1); // ([^&;]*) + final String two = m.group(2); // (?=(;|&|$)) + m.appendReplacement(buf, Matcher.quoteReplacement(checkEntity(one, two))); + } + m.appendTail(buf); + + return encodeQuotes(buf.toString()); + } + + private String encodeQuotes(final String s) { + if (encodeQuotes) { + StringBuffer buf = new StringBuffer(); + Matcher m = P_VALID_QUOTES.matcher(s); + while (m.find()) { + final String one = m.group(1); // (>|^) + final String two = m.group(2); // ([^<]+?) + final String three = m.group(3); // (<|$) + m.appendReplacement(buf, Matcher.quoteReplacement(one + regexReplace(P_QUOTE, """, two) + three)); + } + m.appendTail(buf); + return buf.toString(); + } else { + return s; + } + } + + private String checkEntity(final String preamble, final String term) { + + return ";".equals(term) && isValidEntity(preamble) ? '&' + preamble : "&" + preamble; + } + + private boolean isValidEntity(final String entity) { + return inArray(entity, vAllowedEntities); + } + + private static boolean inArray(final String s, final String[] array) { + for (String item : array) { + if (item != null && item.equals(s)) { + return true; + } + } + return false; + } + + private boolean allowed(final String name) { + return (vAllowed.isEmpty() || vAllowed.containsKey(name)) && !inArray(name, vDisallowed); + } + + private boolean allowedAttribute(final String name, final String paramName) { + return allowed(name) && (vAllowed.isEmpty() || vAllowed.get(name).contains(paramName)); + } +} diff --git a/hutool-http/src/main/java/cn/hutool/http/Header.java b/hutool-http/src/main/java/cn/hutool/http/Header.java new file mode 100644 index 000000000..d1be07669 --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/Header.java @@ -0,0 +1,75 @@ +package cn.hutool.http; + +/** + * Http 头域 + * @author Looly + * + */ +public enum Header { + + //------------------------------------------------------------- 通用头域 + /**提供日期和时间标志,说明报文是什么时间创建的*/ + DATE("Date"), + /**允许客户端和服务器指定与请求/响应连接有关的选项*/ + CONNECTION("Connection"), + /**给出发送端使用的MIME版本*/ + MIME_VERSION("MIME-Version"), + /**如果报文采用了分块传输编码(chunked transfer encoding) 方式,就可以用这个首部列出位于报文拖挂(trailer)部分的首部集合*/ + TRAILER("Trailer"), + /**告知接收端为了保证报文的可靠传输,对报文采用了什么编码方式*/ + TRANSFER_ENCODING("Transfer-Encoding"), + /**给出了发送端可能想要"升级"使用的新版本和协议*/ + UPGRADE("Upgrade"), + /**显示了报文经过的中间节点*/ + VIA("Via"), + /**指定请求和响应遵循的缓存机制*/ + CACHE_CONTROL("Cache-Control"), + /**用来包含实现特定的指令,最常用的是Pragma:no-cache。在HTTP/1.1协议中,它的含义和Cache- Control:no-cache相同*/ + PRAGMA("Pragma"), + /**请求表示提交内容类型或返回返回内容的MIME类型*/ + CONTENT_TYPE("Content-Type"), + + //------------------------------------------------------------- 请求头域 + /**指定请求资源的Intenet主机和端口号,必须表示请求url的原始服务器或网关的位置。HTTP/1.1请求必须包含主机头域,否则系统会以400状态码返回*/ + HOST("Host"), + /**允许客户端指定请求uri的源资源地址,这可以允许服务器生成回退链表,可用来登陆、优化cache等。他也允许废除的或错误的连接由于维护的目的被 追踪。如果请求的uri没有自己的uri地址,Referer不能被发送。如果指定的是部分uri地址,则此地址应该是一个相对地址*/ + REFERER("Referer"), + /** 指定请求的域 */ + ORIGIN("Origin"), + /**HTTP客户端运行的浏览器类型的详细信息。通过该头部信息,web服务器可以判断到当前HTTP请求的客户端浏览器类别*/ + USER_AGENT("User-Agent"), + /**指定客户端能够接收的内容类型,内容类型中的先后次序表示客户端接收的先后次序*/ + ACCEPT("Accept"), + /**指定HTTP客户端浏览器用来展示返回信息所优先选择的语言*/ + ACCEPT_LANGUAGE("Accept-Language"), + /**指定客户端浏览器可以支持的web服务器返回内容压缩编码类型*/ + ACCEPT_ENCODING("Accept-Encoding"), + /**浏览器可以接受的字符编码集*/ + ACCEPT_CHARSET("Accept-Charset"), + /**HTTP请求发送时,会把保存在该请求域名下的所有cookie值一起发送给web服务器*/ + COOKIE("Cookie"), + /**请求的内容长度*/ + CONTENT_LENGTH("Content-Length"), + + //------------------------------------------------------------- 响应头域 + /**Cookie*/ + SET_COOKIE("Set-Cookie"), + /**Content-Encoding*/ + CONTENT_ENCODING("Content-Encoding"), + /**Content-Disposition*/ + CONTENT_DISPOSITION("Content-Disposition"), + /**ETag*/ + ETAG("ETag"), + /** 重定向指示到的URL */ + LOCATION("Location"); + + private String value; + private Header(String value) { + this.value = value; + } + + @Override + public String toString() { + return value; + } +} diff --git a/hutool-http/src/main/java/cn/hutool/http/HtmlUtil.java b/hutool-http/src/main/java/cn/hutool/http/HtmlUtil.java new file mode 100644 index 000000000..74a1433f7 --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/HtmlUtil.java @@ -0,0 +1,203 @@ +package cn.hutool.http; + +import cn.hutool.core.util.EscapeUtil; +import cn.hutool.core.util.ReUtil; +import cn.hutool.core.util.StrUtil; + +/** + * HTML工具类 + * + * @author xiaoleilu + * + */ +public class HtmlUtil { + + public static final String NBSP = StrUtil.HTML_NBSP; + public static final String AMP = StrUtil.HTML_AMP; + public static final String QUOTE = StrUtil.HTML_QUOTE; + public static final String APOS = StrUtil.HTML_APOS; + public static final String LT = StrUtil.HTML_LT; + public static final String GT = StrUtil.HTML_GT; + + public static final String RE_HTML_MARK = "(<[^<]*?>)|(<[\\s]*?/[^<]*?>)|(<[^<]*?/[\\s]*?>)"; + public static final String RE_SCRIPT = "<[\\s]*?script[^>]*?>.*?<[\\s]*?\\/[\\s]*?script[\\s]*?>"; + + private static final char[][] TEXT = new char[64][]; + + static { + for (int i = 0; i < 64; i++) { + TEXT[i] = new char[] { (char) i }; + } + + // special HTML characters + TEXT['\''] = "'".toCharArray(); // 单引号 (''' doesn't work - it is not by the w3 specs) + TEXT['"'] = QUOTE.toCharArray(); // 单引号 + TEXT['&'] = AMP.toCharArray(); // &符 + TEXT['<'] = LT.toCharArray(); // 小于号 + TEXT['>'] = GT.toCharArray(); // 大于号 + } + + /** + * 转义文本中的HTML字符为安全的字符,以下字符被转义: + *
    + *
  • ' 替换为 &#039; (&apos; doesn't work in HTML4)
  • + *
  • " 替换为 &quot;
  • + *
  • & 替换为 &amp;
  • + *
  • < 替换为 &lt;
  • + *
  • > 替换为 &gt;
  • + *
+ * + * @param text 被转义的文本 + * @return 转义后的文本 + */ + public static String escape(String text) { + return encode(text, TEXT); + } + + /** + * 还原被转义的HTML特殊字符 + * + * @param htmlStr 包含转义符的HTML内容 + * @return 转换后的字符串 + */ + public static String unescape(String htmlStr) { + if (StrUtil.isBlank(htmlStr)) { + return htmlStr; + } + + return EscapeUtil.unescapeHtml4(htmlStr); + } + + // ---------------------------------------------------------------- encode text + + /** + * 清除所有HTML标签 + * + * @param content 文本 + * @return 清除标签后的文本 + */ + public static String cleanHtmlTag(String content) { + return content.replaceAll(RE_HTML_MARK, ""); + } + + /** + * 清除指定HTML标签和被标签包围的内容
+ * 不区分大小写 + * + * @param content 文本 + * @param tagNames 要清除的标签 + * @return 去除标签后的文本 + */ + public static String removeHtmlTag(String content, String... tagNames) { + return removeHtmlTag(content, true, tagNames); + } + + /** + * 清除指定HTML标签,不包括内容
+ * 不区分大小写 + * + * @param content 文本 + * @param tagNames 要清除的标签 + * @return 去除标签后的文本 + */ + public static String unwrapHtmlTag(String content, String... tagNames) { + return removeHtmlTag(content, false, tagNames); + } + + /** + * 清除指定HTML标签
+ * 不区分大小写 + * + * @param content 文本 + * @param withTagContent 是否去掉被包含在标签中的内容 + * @param tagNames 要清除的标签 + * @return 去除标签后的文本 + */ + public static String removeHtmlTag(String content, boolean withTagContent, String... tagNames) { + String regex = null; + for (String tagName : tagNames) { + if (StrUtil.isBlank(tagName)) { + continue; + } + tagName = tagName.trim(); + // (?i)表示其后面的表达式忽略大小写 + if (withTagContent) { + // 标签及其包含内容 + regex = StrUtil.format("(?i)<{}\\s*?[^>]*?/?>(.*?)?", tagName, tagName); + } else { + // 标签不包含内容 + regex = StrUtil.format("(?i)<{}\\s*?[^>]*?>|", tagName, tagName); + } + + content = ReUtil.delAll(regex, content); // 非自闭标签小写 + } + return content; + } + + /** + * 去除HTML标签中的属性 + * + * @param content 文本 + * @param attrs 属性名(不区分大小写) + * @return 处理后的文本 + */ + public static String removeHtmlAttr(String content, String... attrs) { + String regex = null; + for (String attr : attrs) { + regex = StrUtil.format("(?i)\\s*{}=([\"']).*?\\1", attr); + content = content.replaceAll(regex, StrUtil.EMPTY); + } + return content; + } + + /** + * 去除指定标签的所有属性 + * + * @param content 内容 + * @param tagNames 指定标签 + * @return 处理后的文本 + */ + public static String removeAllHtmlAttr(String content, String... tagNames) { + String regex = null; + for (String tagName : tagNames) { + regex = StrUtil.format("(?i)<{}[^>]*?>", tagName); + content = content.replaceAll(regex, StrUtil.format("<{}>", tagName)); + } + return content; + } + + /** + * Encoder + * + * @param text 被编码的文本 + * @param array 特殊字符集合 + * @return 编码后的字符 + */ + private static String encode(String text, char[][] array) { + int len; + if ((text == null) || ((len = text.length()) == 0)) { + return StrUtil.EMPTY; + } + StringBuilder buffer = new StringBuilder(len + (len >> 2)); + char c; + for (int i = 0; i < len; i++) { + c = text.charAt(i); + if (c < 64) { + buffer.append(array[c]); + } else { + buffer.append(c); + } + } + return buffer.toString(); + } + + /** + * 过滤HTML文本,防止XSS攻击 + * + * @param htmlContent HTML内容 + * @return 过滤后的内容 + */ + public static String filter(String htmlContent) { + return new HTMLFilter().filter(htmlContent); + } +} diff --git a/hutool-http/src/main/java/cn/hutool/http/HttpBase.java b/hutool-http/src/main/java/cn/hutool/http/HttpBase.java new file mode 100644 index 000000000..3893fbd36 --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/HttpBase.java @@ -0,0 +1,286 @@ +package cn.hutool.http; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.map.CaseInsensitiveMap; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; + +/** + * http基类 + * @author Looly + * @param 子类类型,方便链式编程 + */ +@SuppressWarnings("unchecked") +public abstract class HttpBase { + + /**HTTP/1.0*/ + public static final String HTTP_1_0 = "HTTP/1.0"; + /**HTTP/1.1*/ + public static final String HTTP_1_1 = "HTTP/1.1"; + + /**存储头信息*/ + protected Map> headers = new HashMap>(); + /**编码*/ + protected Charset charset = CharsetUtil.CHARSET_UTF_8; + /**http版本*/ + protected String httpVersion = HTTP_1_1; + /**存储主体*/ + protected byte[] bodyBytes; + + // ---------------------------------------------------------------- Headers start + /** + * 根据name获取头信息
+ * 根据RFC2616规范,header的name不区分大小写 + * + * @param name Header名 + * @return Header值 + */ + public String header(String name) { + final List values = headerList(name); + if(CollectionUtil.isEmpty(values)) { + return null; + } + return values.get(0); + } + + /** + * 根据name获取头信息列表 + * @param name Header名 + * @return Header值 + * @since 3.1.1 + */ + public List headerList(String name) { + if(StrUtil.isBlank(name)) { + return null; + } + + final CaseInsensitiveMap> headersIgnoreCase = new CaseInsensitiveMap<>(this.headers); + return headersIgnoreCase.get(name.trim()); + } + + /** + * 根据name获取头信息 + * @param name Header名 + * @return Header值 + */ + public String header(Header name) { + if(null == name) { + return null; + } + return header(name.toString()); + } + + /** + * 设置一个header
+ * 如果覆盖模式,则替换之前的值,否则加入到值列表中 + * @param name Header名 + * @param value Header值 + * @param isOverride 是否覆盖已有值 + * @return T 本身 + */ + public T header(String name, String value, boolean isOverride) { + if(null != name && null != value){ + final List values = headers.get(name.trim()); + if(isOverride || CollectionUtil.isEmpty(values)) { + final ArrayList valueList = new ArrayList(); + valueList.add(value); + headers.put(name.trim(), valueList); + }else { + values.add(value.trim()); + } + } + return (T) this; + } + + /** + * 设置一个header
+ * 如果覆盖模式,则替换之前的值,否则加入到值列表中 + * @param name Header名 + * @param value Header值 + * @param isOverride 是否覆盖已有值 + * @return T 本身 + */ + public T header(Header name, String value, boolean isOverride) { + return header(name.toString(), value, isOverride); + } + + /** + * 设置一个header
+ * 覆盖模式,则替换之前的值 + * @param name Header名 + * @param value Header值 + * @return T 本身 + */ + public T header(Header name, String value) { + return header(name.toString(), value, true); + } + + /** + * 设置一个header
+ * 覆盖模式,则替换之前的值 + * @param name Header名 + * @param value Header值 + * @return T 本身 + */ + public T header(String name, String value) { + return header(name, value, true); + } + + /** + * 设置请求头
+ * 不覆盖原有请求头 + * + * @param headers 请求头 + * @return this + */ + public T header(Map> headers) { + return header(headers, false); + } + + /** + * 设置请求头
+ * 不覆盖原有请求头 + * + * @param headers 请求头 + * @param isOverride 是否覆盖已有头信息 + * @return this + * @since 4.0.8 + */ + public T header(Map> headers, boolean isOverride) { + if(CollectionUtil.isEmpty(headers)) { + return (T)this; + } + + String name; + for (Entry> entry : headers.entrySet()) { + name = entry.getKey(); + for (String value : entry.getValue()) { + this.header(name, StrUtil.nullToEmpty(value), isOverride); + } + } + return (T)this; + } + + /** + * 新增请求头
+ * 不覆盖原有请求头 + * + * @param headers 请求头 + * @return this + * @since 4.0.3 + */ + public T addHeaders(Map headers) { + if(CollectionUtil.isEmpty(headers)) { + return (T)this; + } + + for (Entry entry : headers.entrySet()) { + this.header(entry.getKey(), StrUtil.nullToEmpty(entry.getValue()), false); + } + return (T)this; + } + + /** + * 移除一个头信息 + * @param name Header名 + * @return this + */ + public T removeHeader(String name) { + if(name != null) { + headers.remove(name.trim()); + } + return (T)this; + } + + /** + * 移除一个头信息 + * @param name Header名 + * @return this + */ + public T removeHeader(Header name) { + return removeHeader(name.toString()); + } + + /** + * 获取headers + * @return Headers Map + */ + public Map> headers() { + return Collections.unmodifiableMap(headers); + } + // ---------------------------------------------------------------- Headers end + + /** + * 返回http版本 + * @return String + */ + public String httpVersion() { + return httpVersion; + } + /** + * 设置http版本 + * + * @param httpVersion Http版本,{@link HttpBase#HTTP_1_0},{@link HttpBase#HTTP_1_1} + * @return this + */ + public T httpVersion(String httpVersion) { + this.httpVersion = httpVersion; + return (T) this; + } + + /** + * 返回字符集 + * @return 字符集 + */ + public String charset() { + return charset.name(); + } + + /** + * 设置字符集 + * @param charset 字符集 + * @return T 自己 + * @see CharsetUtil + */ + public T charset(String charset) { + if(StrUtil.isNotBlank(charset)){ + this.charset = Charset.forName(charset); + } + return (T) this; + } + + /** + * 设置字符集 + * @param charset 字符集 + * @return T 自己 + * @see CharsetUtil + */ + public T charset(Charset charset) { + if(null != charset){ + this.charset = charset; + } + return (T) this; + } + + @Override + public String toString() { + StringBuilder sb = StrUtil.builder(); + sb.append("Request Headers: ").append(StrUtil.CRLF); + for (Entry> entry : this.headers.entrySet()) { + sb.append(" ").append(entry).append(StrUtil.CRLF); + } + + sb.append("Request Body: ").append(StrUtil.CRLF); + sb.append(" ").append(StrUtil.str(this.bodyBytes, this.charset)).append(StrUtil.CRLF); + + return sb.toString(); + } +} diff --git a/hutool-http/src/main/java/cn/hutool/http/HttpConnection.java b/hutool-http/src/main/java/cn/hutool/http/HttpConnection.java new file mode 100644 index 000000000..db6663fbe --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/HttpConnection.java @@ -0,0 +1,543 @@ +package cn.hutool.http; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.ProtocolException; +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.Charset; +import java.nio.charset.UnsupportedCharsetException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLSocketFactory; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; +import cn.hutool.http.ssl.AndroidSupportSSLFactory; +import cn.hutool.http.ssl.SSLSocketFactoryBuilder; +import cn.hutool.http.ssl.TrustAnyHostnameVerifier; +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; + +/** + * http连接对象,对HttpURLConnection的包装 + * + * @author Looly + * + */ +public class HttpConnection { + private final static Log log = LogFactory.get(); + + private URL url; + private Proxy proxy; + private HttpURLConnection conn; + + /** + * 创建HttpConnection + * + * @param urlStr URL + * @param proxy 代理,无代理传{@code null} + * @return HttpConnection + */ + public static HttpConnection create(String urlStr, Proxy proxy) { + return create(URLUtil.toUrlForHttp(urlStr), proxy); + } + + /** + * 创建HttpConnection + * + * @param url URL + * @param proxy 代理,无代理传{@code null} + * @return HttpConnection + */ + public static HttpConnection create(URL url, Proxy proxy) { + return new HttpConnection(url, proxy); + } + + // --------------------------------------------------------------- Constructor start + /** + * 构造HttpConnection + * + * @param url URL + * @param proxy 代理 + */ + public HttpConnection(URL url, Proxy proxy) { + this.url = url; + this.proxy = proxy; + + // 初始化Http连接 + initConn(); + } + + // --------------------------------------------------------------- Constructor end + + /** + * 初始化连接相关信息 + * + * @return HttpConnection + * @since 4.4.1 + */ + public HttpConnection initConn() { + try { + this.conn = openHttp(); + } catch (IOException e) { + throw new HttpException(e); + } + + return this; + } + + // --------------------------------------------------------------- Getters And Setters start + /** + * 获取请求方法,GET/POST + * + * @return 请求方法,GET/POST + */ + public Method getMethod() { + return Method.valueOf(this.conn.getRequestMethod()); + } + + /** + * 设置请求方法 + * + * @param method 请求方法 + * @return 自己 + */ + public HttpConnection setMethod(Method method) { + // method + try { + this.conn.setRequestMethod(method.toString()); + } catch (ProtocolException e) { + throw new HttpException(e); + } + + // do input and output + this.conn.setDoInput(true); + if (Method.POST.equals(method) // + || Method.PUT.equals(method)// + || Method.PATCH.equals(method)// + || Method.DELETE.equals(method)) { + this.conn.setDoOutput(true); + this.conn.setUseCaches(false); + } + return this; + } + + /** + * 获取请求URL + * + * @return 请求URL + */ + public URL getUrl() { + return url; + } + + /** + * 获得代理 + * + * @return {@link Proxy} + */ + public Proxy getProxy() { + return proxy; + } + + /** + * 获取HttpURLConnection对象 + * + * @return HttpURLConnection + */ + public HttpURLConnection getHttpURLConnection() { + return conn; + } + + // --------------------------------------------------------------- Getters And Setters end + + // ---------------------------------------------------------------- Headers start + /** + * 设置请求头
+ * 当请求头存在时,覆盖之 + * + * @param header 头名 + * @param value 头值 + * @param isOverride 是否覆盖旧值 + * @return HttpConnection + */ + public HttpConnection header(String header, String value, boolean isOverride) { + if (null != this.conn) { + if (isOverride) { + this.conn.setRequestProperty(header, value); + } else { + this.conn.addRequestProperty(header, value); + } + } + + return this; + } + + /** + * 设置请求头
+ * 当请求头存在时,覆盖之 + * + * @param header 头名 + * @param value 头值 + * @param isOverride 是否覆盖旧值 + * @return HttpConnection + */ + public HttpConnection header(Header header, String value, boolean isOverride) { + return header(header.toString(), value, isOverride); + } + + /** + * 设置请求头
+ * 不覆盖原有请求头 + * + * @param headerMap 请求头 + * @param isOverride 是否覆盖 + * @return this + */ + public HttpConnection header(Map> headerMap, boolean isOverride) { + if (MapUtil.isNotEmpty(headerMap)) { + String name; + for (Entry> entry : headerMap.entrySet()) { + name = entry.getKey(); + for (String value : entry.getValue()) { + this.header(name, StrUtil.nullToEmpty(value), isOverride); + } + } + } + return this; + } + + /** + * 获取Http请求头 + * + * @param name Header名 + * @return Http请求头值 + */ + public String header(String name) { + return this.conn.getHeaderField(name); + } + + /** + * 获取Http请求头 + * + * @param name Header名 + * @return Http请求头值 + */ + public String header(Header name) { + return header(name.toString()); + } + + /** + * 获取所有Http请求头 + * + * @return Http请求头Map + */ + public Map> headers() { + return this.conn.getHeaderFields(); + } + + // ---------------------------------------------------------------- Headers end + + /** + * 设置https请求参数
+ * 有些时候htts请求会出现com.sun.net.ssl.internal.www.protocol.https.HttpsURLConnectionOldImpl的实现,此为sun内部api,按照普通http请求处理 + * + * @param hostnameVerifier 域名验证器,非https传入null + * @param ssf SSLSocketFactory,非https传入null + * @return this + * @throws HttpException KeyManagementException和NoSuchAlgorithmException异常包装 + */ + public HttpConnection setHttpsInfo(HostnameVerifier hostnameVerifier, SSLSocketFactory ssf) throws HttpException { + final HttpURLConnection conn = this.conn; + + if (conn instanceof HttpsURLConnection) { + // Https请求 + final HttpsURLConnection httpsConn = (HttpsURLConnection) conn; + // 验证域 + httpsConn.setHostnameVerifier(null != hostnameVerifier ? hostnameVerifier : new TrustAnyHostnameVerifier()); + if (null == ssf) { + try { + if (StrUtil.equalsIgnoreCase("dalvik", System.getProperty("java.vm.name"))) { + // 兼容android低版本SSL连接 + ssf = new AndroidSupportSSLFactory(); + } else { + ssf = SSLSocketFactoryBuilder.create().build(); + } + } catch (KeyManagementException | NoSuchAlgorithmException e) { + throw new HttpException(e); + } + } + httpsConn.setSSLSocketFactory(ssf); + } + + return this; + } + + /** + * 关闭缓存 + * + * @return this + */ + public HttpConnection disableCache() { + this.conn.setUseCaches(false); + return this; + } + + /** + * 设置连接超时 + * + * @param timeout 超时 + * @return this + */ + public HttpConnection setConnectTimeout(int timeout) { + if (timeout > 0 && null != this.conn) { + this.conn.setConnectTimeout(timeout); + } + + return this; + } + + /** + * 设置读取超时 + * + * @param timeout 超时 + * @return this + */ + public HttpConnection setReadTimeout(int timeout) { + if (timeout > 0 && null != this.conn) { + this.conn.setReadTimeout(timeout); + } + + return this; + } + + /** + * 设置连接和读取的超时时间 + * + * @param timeout 超时时间 + * @return this + */ + public HttpConnection setConnectionAndReadTimeout(int timeout) { + setConnectTimeout(timeout); + setReadTimeout(timeout); + + return this; + } + + /** + * 设置Cookie + * + * @param cookie Cookie + * @return this + */ + public HttpConnection setCookie(String cookie) { + if (cookie != null) { + log.debug("With Cookie: {}", cookie); + header(Header.COOKIE, cookie, true); + } + return this; + } + + /** + * 采用流方式上传数据,无需本地缓存数据。
+ * HttpUrlConnection默认是将所有数据读到本地缓存,然后再发送给服务器,这样上传大文件时就会导致内存溢出。 + * + * @param blockSize 块大小(bytes数) + * @return this + */ + public HttpConnection setChunkedStreamingMode(int blockSize) { + conn.setChunkedStreamingMode(blockSize); + return this; + } + + /** + * 设置自动HTTP 30X跳转 + * + * @param isInstanceFollowRedirects 是否自定跳转 + * @return this + */ + public HttpConnection setInstanceFollowRedirects(boolean isInstanceFollowRedirects) { + conn.setInstanceFollowRedirects(isInstanceFollowRedirects); + return this; + } + + /** + * 连接 + * + * @return this + * @throws IOException IO异常 + */ + public HttpConnection connect() throws IOException { + if (null != this.conn) { + this.conn.connect(); + } + return this; + } + + /** + * 静默断开连接。不抛出异常 + * + * @return this + * @since 4.6.0 + */ + public HttpConnection disconnectQuietly() { + try { + disconnect(); + } catch (Throwable e) { + // ignore + } + + return this; + } + + /** + * 断开连接 + * + * @return this + */ + public HttpConnection disconnect() { + if (null != this.conn) { + this.conn.disconnect(); + } + return this; + } + + /** + * 获得输入流对象
+ * 输入流对象用于读取数据 + * + * @return 输入流对象 + * @throws IOException IO异常 + */ + public InputStream getInputStream() throws IOException { + if (null != this.conn) { + return this.conn.getInputStream(); + } + return null; + } + + /** + * 当返回错误代码时,获得错误内容流 + * + * @return 错误内容 + * @throws IOException IO异常 + */ + public InputStream getErrorStream() throws IOException { + if (null != this.conn) { + return this.conn.getErrorStream(); + } + return null; + } + + /** + * 获取输出流对象 输出流对象用于发送数据 + * + * @return OutputStream + * @throws IOException IO异常 + */ + public OutputStream getOutputStream() throws IOException { + if (null == this.conn) { + throw new IOException("HttpURLConnection has not been initialized."); + } + return this.conn.getOutputStream(); + } + + /** + * 获取响应码 + * + * @return 响应码 + * @throws IOException IO异常 + */ + public int responseCode() throws IOException { + if (null != this.conn) { + return this.conn.getResponseCode(); + } + return 0; + } + + /** + * 获得字符集编码
+ * 从Http连接的头信息中获得字符集
+ * 从ContentType中获取 + * + * @return 字符集编码 + */ + public String getCharsetName() { + return HttpUtil.getCharset(conn); + } + + /** + * 获取字符集编码
+ * 从Http连接的头信息中获得字符集
+ * 从ContentType中获取 + * + * @return {@link Charset}编码 + * @since 3.0.9 + */ + public Charset getCharset() { + Charset charset = null; + final String charsetName = getCharsetName(); + if (StrUtil.isNotBlank(charsetName)) { + try { + charset = Charset.forName(charsetName); + } catch (UnsupportedCharsetException e) { + // ignore + } + } + return charset; + } + + @Override + public String toString() { + StringBuilder sb = StrUtil.builder(); + sb.append("Request URL: ").append(this.url).append(StrUtil.CRLF); + sb.append("Request Method: ").append(this.getMethod()).append(StrUtil.CRLF); + // sb.append("Request Headers: ").append(StrUtil.CRLF); + // for (Entry> entry : this.conn.getHeaderFields().entrySet()) { + // sb.append(" ").append(entry).append(StrUtil.CRLF); + // } + + return sb.toString(); + } + + // --------------------------------------------------------------- Private Method start + /** + * 初始化http或https请求参数
+ * 有些时候htts请求会出现com.sun.net.ssl.internal.www.protocol.https.HttpsURLConnectionOldImpl的实现,此为sun内部api,按照普通http请求处理 + * + * @param hostnameVerifier 域名验证器,非https传入null + * @param ssf SSLSocketFactory,非https传入null + * @return {@link HttpURLConnection},https返回{@link HttpsURLConnection} + */ + private HttpURLConnection openHttp() throws IOException { + final URLConnection conn = openConnection(); + if (false == conn instanceof HttpURLConnection) { + // 防止其它协议造成的转换异常 + throw new HttpException("'{}' is not a http connection, make sure URL is format for http.", conn.getClass().getName()); + } + + return (HttpURLConnection) conn; + } + + /** + * 建立连接 + * + * @return {@link URLConnection} + * @throws IOException + */ + private URLConnection openConnection() throws IOException { + return (null == this.proxy) ? url.openConnection() : url.openConnection(this.proxy); + } + // --------------------------------------------------------------- Private Method end +} \ No newline at end of file diff --git a/hutool-http/src/main/java/cn/hutool/http/HttpException.java b/hutool-http/src/main/java/cn/hutool/http/HttpException.java new file mode 100644 index 000000000..92f00eecd --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/HttpException.java @@ -0,0 +1,31 @@ +package cn.hutool.http; + +import cn.hutool.core.util.StrUtil; + +/** + *HTTP异常 + * @author xiaoleilu + */ +public class HttpException extends RuntimeException{ + private static final long serialVersionUID = 8247610319171014183L; + + public HttpException(Throwable e) { + super(e.getMessage(), e); + } + + public HttpException(String message) { + super(message); + } + + public HttpException(String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params)); + } + + public HttpException(String message, Throwable throwable) { + super(message, throwable); + } + + public HttpException(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } +} diff --git a/hutool-http/src/main/java/cn/hutool/http/HttpGlobalConfig.java b/hutool-http/src/main/java/cn/hutool/http/HttpGlobalConfig.java new file mode 100755 index 000000000..92d9b6065 --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/HttpGlobalConfig.java @@ -0,0 +1,66 @@ +package cn.hutool.http; + +import java.net.CookieManager; + +import cn.hutool.http.cookie.GlobalCookieManager; + +/** + * HTTP 全局参数配置 + * + * @author Looly + * @since 4.6.2 + */ +public class HttpGlobalConfig { + + protected static int timeout = -1; + + /** + * 获取全局默认的超时时长 + * + * @return 全局默认的超时时长 + */ + public static int getTimeout() { + return timeout; + } + + /** + * 设置默认的连接和读取超时时长 + * + * @param customTimeout 超时时长 + */ + public static void setTimeout(int customTimeout) { + timeout = customTimeout; + } + + /** + * 获取Cookie管理器,用于自定义Cookie管理 + * + * @return {@link CookieManager} + * @since 4.1.0 + * @see GlobalCookieManager#getCookieManager() + */ + public static CookieManager getCookieManager() { + return GlobalCookieManager.getCookieManager(); + } + + /** + * 自定义{@link CookieManager} + * + * @param customCookieManager 自定义的{@link CookieManager} + * @since 4.5.14 + * @see GlobalCookieManager#setCookieManager(CookieManager) + */ + public static void setCookieManager(CookieManager customCookieManager) { + GlobalCookieManager.setCookieManager(customCookieManager); + } + + /** + * 关闭Cookie + * + * @since 4.1.9 + * @see GlobalCookieManager#setCookieManager(CookieManager) + */ + public static void closeCookie() { + GlobalCookieManager.setCookieManager(null); + } +} diff --git a/hutool-http/src/main/java/cn/hutool/http/HttpRequest.java b/hutool-http/src/main/java/cn/hutool/http/HttpRequest.java new file mode 100644 index 000000000..1d27af2b0 --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/HttpRequest.java @@ -0,0 +1,1159 @@ +package cn.hutool.http; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.CookieManager; +import java.net.HttpCookie; +import java.net.HttpURLConnection; +import java.net.Proxy; +import java.net.URLStreamHandler; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSocketFactory; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.io.resource.BytesResource; +import cn.hutool.core.io.resource.FileResource; +import cn.hutool.core.io.resource.MultiFileResource; +import cn.hutool.core.io.resource.MultiResource; +import cn.hutool.core.io.resource.Resource; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; +import cn.hutool.http.cookie.GlobalCookieManager; +import cn.hutool.http.ssl.SSLSocketFactoryBuilder; +import cn.hutool.json.JSON; +import cn.hutool.log.StaticLog; + +/** + * http请求类
+ * Http请求类用于构建Http请求并同步获取结果,此类通过CookieManager持有域名对应的Cookie值,再次请求时会自动附带Cookie信息 + * + * @author Looly + */ +public class HttpRequest extends HttpBase { + + private static final String BOUNDARY = "--------------------Hutool_" + RandomUtil.randomString(16); + private static final byte[] BOUNDARY_END = StrUtil.format("--{}--\r\n", BOUNDARY).getBytes(); + private static final String CONTENT_DISPOSITION_TEMPLATE = "Content-Disposition: form-data; name=\"{}\"\r\n\r\n"; + private static final String CONTENT_DISPOSITION_FILE_TEMPLATE = "Content-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\n"; + + private static final String CONTENT_TYPE_MULTIPART_PREFIX = "multipart/form-data; boundary="; + private static final String CONTENT_TYPE_FILE_TEMPLATE = "Content-Type: {}\r\n\r\n"; + + /** + * 设置全局默认的连接和读取超时时长 + * + * @param customTimeout 超时时长 + * @see HttpGlobalConfig#setTimeout(int) + * @since 4.6.2 + */ + public static void setGlobalTimeout(int customTimeout) { + HttpGlobalConfig.setTimeout(customTimeout); + } + + /** + * 获取Cookie管理器,用于自定义Cookie管理 + * + * @return {@link CookieManager} + * @since 4.1.0 + * @see GlobalCookieManager#getCookieManager() + */ + public static CookieManager getCookieManager() { + return GlobalCookieManager.getCookieManager(); + } + + /** + * 自定义{@link CookieManager} + * + * @param customCookieManager 自定义的{@link CookieManager} + * @since 4.5.14 + * @see GlobalCookieManager#setCookieManager(CookieManager) + */ + public static void setCookieManager(CookieManager customCookieManager) { + GlobalCookieManager.setCookieManager(customCookieManager); + } + + /** + * 关闭Cookie + * + * @since 4.1.9 + * @see GlobalCookieManager#setCookieManager(CookieManager) + */ + public static void closeCookie() { + GlobalCookieManager.setCookieManager(null); + } + + private String url; + private URLStreamHandler urlHandler; + private Method method = Method.GET; + /** 默认连接超时 */ + private int connectionTimeout = HttpGlobalConfig.timeout; + /** 默认读取超时 */ + private int readTimeout = HttpGlobalConfig.timeout; + /** 存储表单数据 */ + private Map form; + /** 文件表单对象,用于文件上传 */ + private Map fileForm; + /** Cookie */ + private String cookie; + + /** 连接对象 */ + private HttpConnection httpConnection; + /** 是否禁用缓存 */ + private boolean isDisableCache; + /** 是否对url中的参数进行编码 */ + private boolean encodeUrlParams; + /** 是否是REST请求模式 */ + private boolean isRest; + /** 重定向次数计数器,内部使用 */ + private int redirectCount; + /** 最大重定向次数 */ + private int maxRedirectCount; + /** 代理 */ + private Proxy proxy; + + /** HostnameVerifier,用于HTTPS安全连接 */ + private HostnameVerifier hostnameVerifier; + /** SSLSocketFactory,用于HTTPS安全连接 */ + private SSLSocketFactory ssf; + + /** + * 构造 + * + * @param url URL + */ + public HttpRequest(String url) { + Assert.notBlank(url, "Param [url] can not be blank !"); + this.url = URLUtil.normalize(url, true); + // 给定一个默认头信息 + this.header(GlobalHeaders.INSTANCE.headers); + } + + // ---------------------------------------------------------------- static Http Method start + /** + * POST请求 + * + * @param url URL + * @return HttpRequest + */ + public static HttpRequest post(String url) { + return new HttpRequest(url).method(Method.POST); + } + + /** + * GET请求 + * + * @param url URL + * @return HttpRequest + */ + public static HttpRequest get(String url) { + return new HttpRequest(url).method(Method.GET); + } + + /** + * HEAD请求 + * + * @param url URL + * @return HttpRequest + */ + public static HttpRequest head(String url) { + return new HttpRequest(url).method(Method.HEAD); + } + + /** + * OPTIONS请求 + * + * @param url URL + * @return HttpRequest + */ + public static HttpRequest options(String url) { + return new HttpRequest(url).method(Method.OPTIONS); + } + + /** + * PUT请求 + * + * @param url URL + * @return HttpRequest + */ + public static HttpRequest put(String url) { + return new HttpRequest(url).method(Method.PUT); + } + + /** + * PATCH请求 + * + * @param url URL + * @return HttpRequest + * @since 3.0.9 + */ + public static HttpRequest patch(String url) { + return new HttpRequest(url).method(Method.PATCH); + } + + /** + * DELETE请求 + * + * @param url URL + * @return HttpRequest + */ + public static HttpRequest delete(String url) { + return new HttpRequest(url).method(Method.DELETE); + } + + /** + * TRACE请求 + * + * @param url URL + * @return HttpRequest + */ + public static HttpRequest trace(String url) { + return new HttpRequest(url).method(Method.TRACE); + } + // ---------------------------------------------------------------- static Http Method end + + /** + * 获取请求URL + * + * @return URL字符串 + * @since 4.1.8 + */ + public String getUrl() { + return url; + } + + /** + * 设置URL + * + * @param url url字符串 + * @since 4.1.8 + */ + public HttpRequest setUrl(String url) { + this.url = url; + return this; + } + + /** + * 设置{@link URLStreamHandler} + *

+ * 部分环境下需要单独设置此项,例如当 WebLogic Server 实例充当 SSL 客户端角色(它会尝试通过 SSL 连接到其他服务器或应用程序)时,
+ * 它会验证 SSL 服务器在数字证书中返回的主机名是否与用于连接 SSL 服务器的 URL 主机名相匹配。如果主机名不匹配,则删除此连接。
+ * 因此weblogic不支持https的sni协议的主机名验证,此时需要将此值设置为sun.net.www.protocol.https.Handler对象。 + *

+ * 相关issue见:https://gitee.com/loolly/hutool/issues/IMD1X + * + * @param urlHandler {@link URLStreamHandler} + * @since 4.1.9 + */ + public HttpRequest setUrlHandler(URLStreamHandler urlHandler) { + this.urlHandler = urlHandler; + return this; + } + + /** + * 获取Http请求方法 + * + * @return {@link Method} + * @since 4.1.8 + */ + public Method getMethod() { + return this.method; + } + + /** + * 设置请求方法 + * + * @param method HTTP方法 + * @return HttpRequest + * @see #method(Method) + * @since 4.1.8 + */ + public HttpRequest setMethod(Method method) { + return method(method); + } + + /** + * 获取{@link HttpConnection} + * + * @return {@link HttpConnection} + * @since 4.2.2 + */ + public HttpConnection getConnection() { + return this.httpConnection; + } + + /** + * 设置请求方法 + * + * @param method HTTP方法 + * @return HttpRequest + */ + public HttpRequest method(Method method) { + if (Method.PATCH == method) { + this.method = Method.POST; + this.header("X-HTTP-Method-Override", "PATCH"); + } else { + this.method = method; + } + + return this; + } + + // ---------------------------------------------------------------- Http Request Header start + /** + * 设置contentType + * + * @param contentType contentType + * @return HttpRequest + */ + public HttpRequest contentType(String contentType) { + header(Header.CONTENT_TYPE, contentType); + return this; + } + + /** + * 设置是否为长连接 + * + * @param isKeepAlive 是否长连接 + * @return HttpRequest + */ + public HttpRequest keepAlive(boolean isKeepAlive) { + header(Header.CONNECTION, isKeepAlive ? "Keep-Alive" : "Close"); + return this; + } + + /** + * @return 获取是否为长连接 + */ + public boolean isKeepAlive() { + String connection = header(Header.CONNECTION); + if (connection == null) { + return !httpVersion.equalsIgnoreCase(HTTP_1_0); + } + + return !connection.equalsIgnoreCase("close"); + } + + /** + * 获取内容长度 + * + * @return String + */ + public String contentLength() { + return header(Header.CONTENT_LENGTH); + } + + /** + * 设置内容长度 + * + * @param value 长度 + * @return HttpRequest + */ + public HttpRequest contentLength(int value) { + header(Header.CONTENT_LENGTH, String.valueOf(value)); + return this; + } + + /** + * 设置Cookie
+ * 自定义Cookie后会覆盖Hutool的默认Cookie行为 + * + * @param cookies Cookie值数组,如果为{@code null}则设置无效,使用默认Cookie行为 + * @return this + * @since 3.1.1 + */ + public HttpRequest cookie(HttpCookie... cookies) { + if (ArrayUtil.isEmpty(cookies)) { + return disableCookie(); + } + return cookie(ArrayUtil.join(cookies, ";")); + } + + /** + * 设置Cookie
+ * 自定义Cookie后会覆盖Hutool的默认Cookie行为 + * + * @param cookie Cookie值,如果为{@code null}则设置无效,使用默认Cookie行为 + * @return this + * @since 3.0.7 + */ + public HttpRequest cookie(String cookie) { + this.cookie = cookie; + return this; + } + + /** + * 禁用默认Cookie行为,此方法调用后会将Cookie置为空。
+ * 如果想重新启用Cookie,请调用:{@link #cookie(String)}方法自定义Cookie。
+ * 如果想启动默认的Cookie行为(自动回填服务器传回的Cookie),则调用{@link #enableDefaultCookie()} + * + * @return this + * @since 3.0.7 + */ + public HttpRequest disableCookie() { + return cookie(StrUtil.EMPTY); + } + + /** + * 打开默认的Cookie行为(自动回填服务器传回的Cookie) + * + * @return this + */ + public HttpRequest enableDefaultCookie() { + return cookie((String) null); + } + // ---------------------------------------------------------------- Http Request Header end + + // ---------------------------------------------------------------- Form start + /** + * 设置表单数据
+ * + * @param name 名 + * @param value 值 + * @return this + */ + public HttpRequest form(String name, Object value) { + if (StrUtil.isBlank(name) || ObjectUtil.isNull(value)) { + return this; // 忽略非法的form表单项内容; + } + + // 停用body + this.bodyBytes = null; + + if (value instanceof File) { + // 文件上传 + return this.form(name, (File) value); + } else if (value instanceof Resource) { + // 自定义流上传 + return this.form(name, (Resource) value); + } else if (this.form == null) { + this.form = new LinkedHashMap<>(); + } + + String strValue; + if (value instanceof List) { + // 列表对象 + strValue = CollectionUtil.join((List) value, ","); + } else if (ArrayUtil.isArray(value)) { + if (File.class == ArrayUtil.getComponentType(value)) { + // 多文件 + return this.form(name, (File[]) value); + } + // 数组对象 + strValue = ArrayUtil.join((Object[]) value, ","); + } else { + // 其他对象一律转换为字符串 + strValue = Convert.toStr(value, null); + } + + form.put(name, strValue); + return this; + } + + /** + * 设置表单数据 + * + * @param name 名 + * @param value 值 + * @param parameters 参数对,奇数为名,偶数为值 + * @return this + * + */ + public HttpRequest form(String name, Object value, Object... parameters) { + form(name, value); + + for (int i = 0; i < parameters.length; i += 2) { + name = parameters[i].toString(); + form(name, parameters[i + 1]); + } + return this; + } + + /** + * 设置map类型表单数据 + * + * @param formMap 表单内容 + * @return this + * + */ + public HttpRequest form(Map formMap) { + if (MapUtil.isNotEmpty(formMap)) { + for (Map.Entry entry : formMap.entrySet()) { + form(entry.getKey(), entry.getValue()); + } + } + return this; + } + + /** + * 文件表单项
+ * 一旦有文件加入,表单变为multipart/form-data + * + * @param name 名 + * @param files 需要上传的文件 + * @return this + */ + public HttpRequest form(String name, File... files) { + if (1 == files.length) { + final File file = files[0]; + return form(name, file, file.getName()); + } + return form(name, new MultiFileResource(files)); + } + + /** + * 文件表单项
+ * 一旦有文件加入,表单变为multipart/form-data + * + * @param name 名 + * @param file 需要上传的文件 + * @return this + */ + public HttpRequest form(String name, File file) { + return form(name, file, file.getName()); + } + + /** + * 文件表单项
+ * 一旦有文件加入,表单变为multipart/form-data + * + * @param name 名 + * @param file 需要上传的文件 + * @param fileName 文件名,为空使用文件默认的文件名 + * @return this + */ + public HttpRequest form(String name, File file, String fileName) { + if (null != file) { + form(name, new FileResource(file, fileName)); + } + return this; + } + + /** + * 文件byte[]表单项
+ * 一旦有文件加入,表单变为multipart/form-data + * + * @param name 名 + * @param fileBytes 需要上传的文件 + * @param fileName 文件名 + * @return this + * @since 4.1.0 + */ + public HttpRequest form(String name, byte[] fileBytes, String fileName) { + if (null != fileBytes) { + form(name, new BytesResource(fileBytes, fileName)); + } + return this; + } + + /** + * 文件表单项
+ * 一旦有文件加入,表单变为multipart/form-data + * + * @param name 名 + * @param resource 数据源,文件可以使用{@link FileResource}包装使用 + * @return this + * @since 4.0.9 + */ + public HttpRequest form(String name, Resource resource) { + if (null != resource) { + if (false == isKeepAlive()) { + keepAlive(true); + } + + if (null == this.fileForm) { + fileForm = new HashMap<>(); + } + // 文件对象 + this.fileForm.put(name, resource); + } + return this; + } + + /** + * 获取表单数据 + * + * @return 表单Map + */ + public Map form() { + return this.form; + } + + /** + * 获取文件表单数据 + * + * @return 文件表单Map + * @since 3.3.0 + */ + public Map fileForm() { + return this.fileForm; + } + // ---------------------------------------------------------------- Form end + + // ---------------------------------------------------------------- Body start + /** + * 设置内容主体 + * + * @param body 请求体 + * @return this + */ + public HttpRequest body(String body) { + return this.body(body, null); + } + + /** + * 设置内容主体
+ * 请求体body参数支持两种类型: + * + *

+	 * 1. 标准参数,例如 a=1&b=2 这种格式
+	 * 2. Rest模式,此时body需要传入一个JSON或者XML字符串,Hutool会自动绑定其对应的Content-Type
+	 * 
+ * + * @param body 请求体 + * @param contentType 请求体类型,{@code null}表示自动判断类型 + * @return this + */ + public HttpRequest body(String body, String contentType) { + body(StrUtil.bytes(body, this.charset)); + this.form = null; // 当使用body时,停止form的使用 + contentLength((null != body ? body.length() : 0)); + + if (null != contentType) { + // Content-Type自定义设置 + this.contentType(contentType); + } else { + // 在用户未自定义的情况下自动根据内容判断 + contentType = HttpUtil.getContentTypeByRequestBody(body); + if (null != contentType && ContentType.isDefault(this.header(Header.CONTENT_TYPE))) { + if (null != this.charset) { + // 附加编码信息 + contentType = ContentType.build(contentType, this.charset); + } + this.contentType(contentType); + } + } + + // 判断是否为rest请求 + if (StrUtil.containsAnyIgnoreCase(contentType, "json", "xml")) { + this.isRest = true; + } + return this; + } + + /** + * 设置JSON内容主体
+ * 设置默认的Content-Type为 application/json 需在此方法调用前使用charset方法设置编码,否则使用默认编码UTF-8 + * + * @param json JSON请求体 + * @return this + */ + public HttpRequest body(JSON json) { + return this.body(json.toString()); + } + + /** + * 设置主体字节码
+ * 需在此方法调用前使用charset方法设置编码,否则使用默认编码UTF-8 + * + * @param bodyBytes 主体 + * @return this + */ + public HttpRequest body(byte[] bodyBytes) { + this.bodyBytes = bodyBytes; + return this; + } + // ---------------------------------------------------------------- Body end + + /** + * 设置超时,单位:毫秒
+ * 超时包括: + * + *
+	 * 1. 连接超时
+	 * 2. 读取响应超时
+	 * 
+ * + * @param milliseconds 超时毫秒数 + * @return this + * @see #setConnectionTimeout(int) + * @see #setReadTimeout(int) + */ + public HttpRequest timeout(int milliseconds) { + setConnectionTimeout(milliseconds); + setReadTimeout(milliseconds); + return this; + } + + /** + * 设置连接超时,单位:毫秒 + * + * @param milliseconds 超时毫秒数 + * @return this + * @since 4.5.6 + */ + public HttpRequest setConnectionTimeout(int milliseconds) { + this.connectionTimeout = milliseconds; + return this; + } + + /** + * 设置连接超时,单位:毫秒 + * + * @param milliseconds 超时毫秒数 + * @return this + * @since 4.5.6 + */ + public HttpRequest setReadTimeout(int milliseconds) { + this.readTimeout = milliseconds; + return this; + } + + /** + * 禁用缓存 + * + * @return this + */ + public HttpRequest disableCache() { + this.isDisableCache = true; + return this; + } + + /** + * 是否对URL中的参数进行编码 + * + * @param isEncodeUrlParams 是否对URL中的参数进行编码 + * @return this + * @since 4.4.1 + */ + public HttpRequest setEncodeUrlParams(boolean isEncodeUrlParams) { + this.encodeUrlParams = isEncodeUrlParams; + return this; + } + + /** + * 设置是否打开重定向,如果打开默认重定向次数为2
+ * 此方法效果与{@link #setMaxRedirectCount(int)} 一致 + * + * @param isFollowRedirects 是否打开重定向 + * @return this + */ + public HttpRequest setFollowRedirects(boolean isFollowRedirects) { + return setMaxRedirectCount(isFollowRedirects ? 2 : 0); + } + + /** + * 设置最大重定向次数
+ * 如果次数小于1则表示不重定向,大于等于1表示打开重定向 + * + * @param maxRedirectCount 最大重定向次数 + * @return this + * @since 3.3.0 + */ + public HttpRequest setMaxRedirectCount(int maxRedirectCount) { + if (maxRedirectCount > 0) { + this.maxRedirectCount = maxRedirectCount; + } else { + this.maxRedirectCount = 0; + } + return this; + } + + /** + * 设置域名验证器
+ * 只针对HTTPS请求,如果不设置,不做验证,所有域名被信任 + * + * @param hostnameVerifier HostnameVerifier + * @return this + */ + public HttpRequest setHostnameVerifier(HostnameVerifier hostnameVerifier) { + // 验证域 + this.hostnameVerifier = hostnameVerifier; + return this; + } + + /** + * 设置代理 + * + * @param proxy 代理 {@link Proxy} + * @return this + */ + public HttpRequest setProxy(Proxy proxy) { + this.proxy = proxy; + return this; + } + + /** + * 设置SSLSocketFactory
+ * 只针对HTTPS请求,如果不设置,使用默认的SSLSocketFactory
+ * 默认SSLSocketFactory为:SSLSocketFactoryBuilder.create().build(); + * + * @param ssf SSLScketFactory + * @return this + */ + public HttpRequest setSSLSocketFactory(SSLSocketFactory ssf) { + this.ssf = ssf; + return this; + } + + /** + * 设置HTTPS安全连接协议,只针对HTTPS请求,可以使用的协议包括:
+ * + *
+	 * 1. TLSv1.2
+	 * 2. TLSv1.1
+	 * 3. SSLv3
+	 * ...
+	 * 
+ * + * @see SSLSocketFactoryBuilder + * @param protocol 协议 + * @return this + */ + public HttpRequest setSSLProtocol(String protocol) { + if (null == this.ssf) { + try { + this.ssf = SSLSocketFactoryBuilder.create().setProtocol(protocol).build(); + } catch (Exception e) { + throw new HttpException(e); + } + } + return this; + } + + /** + * 设置是否rest模式 + * + * @param isRest 是否rest模式 + * @return this + * @since 4.5.0 + */ + public HttpRequest setRest(boolean isRest) { + this.isRest = isRest; + return this; + } + + /** + * 执行Reuqest请求 + * + * @return this + */ + public HttpResponse execute() { + return this.execute(false); + } + + /** + * 异步请求
+ * 异步请求后获取的{@link HttpResponse} 为异步模式,此时此对象持有Http链接(http链接并不会关闭),直调用获取内容方法为止 + * + * @return 异步对象,使用get方法获取HttpResponse对象 + */ + public HttpResponse executeAsync() { + return this.execute(true); + } + + /** + * 执行Reuqest请求 + * + * @param isAsync 是否异步 + * @return this + */ + public HttpResponse execute(boolean isAsync) { + // 初始化URL + urlWithParamIfGet(); + // 编码URL + if (this.encodeUrlParams) { + this.url = HttpUtil.encodeParams(this.url, this.charset); + } + // 初始化 connection + initConnecton(); + + // 发送请求 + send(); + + // 手动实现重定向 + HttpResponse httpResponse = sendRedirectIfPosible(); + + // 获取响应 + if (null == httpResponse) { + httpResponse = new HttpResponse(this.httpConnection, this.charset, isAsync, isIgnoreResponseBody()); + } + return httpResponse; + } + + /** + * 简单验证 + * + * @param username 用户名 + * @param password 密码 + * @return HttpRequest + */ + public HttpRequest basicAuth(String username, String password) { + final String data = username.concat(":").concat(password); + final String base64 = Base64.encode(data, charset); + + header("Authorization", "Basic " + base64, true); + + return this; + } + + // ---------------------------------------------------------------- Private method start + /** + * 初始化网络连接 + */ + private void initConnecton() { + if(null != this.httpConnection) { + // 执行下次请求时自动关闭上次请求(常用于转发) + this.httpConnection.disconnectQuietly(); + } + + this.httpConnection = HttpConnection.create(URLUtil.toUrlForHttp(this.url, this.urlHandler), this.proxy)// + .setMethod(this.method)// + .setHttpsInfo(this.hostnameVerifier, this.ssf)// + .setConnectTimeout(this.connectionTimeout)// + .setReadTimeout(this.readTimeout)// + // 自定义Cookie + .setCookie(this.cookie) + // 定义转发 + .setInstanceFollowRedirects(this.maxRedirectCount > 0 ? true : false) + // 覆盖默认Header + .header(this.headers, true); + + // 读取全局Cookie信息并附带到请求中 + GlobalCookieManager.add(this.httpConnection); + + // 是否禁用缓存 + if (this.isDisableCache) { + this.httpConnection.disableCache(); + } + } + + /** + * 对于GET请求将参数加到URL中
+ * 此处不对URL中的特殊字符做单独编码 + */ + private void urlWithParamIfGet() { + if (Method.GET.equals(method) && false == this.isRest) { + // 优先使用body形式的参数,不存在使用form + if (ArrayUtil.isNotEmpty(this.bodyBytes)) { + this.url = HttpUtil.urlWithForm(this.url, StrUtil.str(this.bodyBytes, this.charset), this.charset, false); + } else { + this.url = HttpUtil.urlWithForm(this.url, this.form, this.charset, false); + } + } + } + + /** + * 调用转发,如果需要转发返回转发结果,否则返回null + * + * @return {@link HttpResponse},无转发返回 null + */ + private HttpResponse sendRedirectIfPosible() { + if (this.maxRedirectCount < 1) { + // 不重定向 + return null; + } + + // 手动实现重定向 + if (this.httpConnection.getHttpURLConnection().getInstanceFollowRedirects()) { + int responseCode; + try { + responseCode = httpConnection.responseCode(); + } catch (IOException e) { + // 错误时静默关闭连接 + this.httpConnection.disconnectQuietly(); + throw new HttpException(e); + } + if (responseCode != HttpURLConnection.HTTP_OK) { + if (responseCode == HttpURLConnection.HTTP_MOVED_TEMP || responseCode == HttpURLConnection.HTTP_MOVED_PERM || responseCode == HttpURLConnection.HTTP_SEE_OTHER) { + this.url = httpConnection.header(Header.LOCATION); + if (redirectCount < this.maxRedirectCount) { + redirectCount++; + return execute(); + } else { + StaticLog.warn("URL [{}] redirect count more than {} !", this.url, this.maxRedirectCount); + } + } + } + } + return null; + } + + /** + * 发送数据流 + * + * @throws IOException + */ + private void send() throws HttpException { + try { + if (Method.POST.equals(this.method) || Method.PUT.equals(this.method) || Method.DELETE.equals(this.method) || this.isRest) { + if (CollectionUtil.isEmpty(this.fileForm)) { + sendFormUrlEncoded();// 普通表单 + } else { + sendMultipart(); // 文件上传表单 + } + } else { + this.httpConnection.connect(); + } + } catch (IOException e) { + // 异常时关闭连接 + this.httpConnection.disconnectQuietly(); + throw new HttpException(e); + } + } + + /** + * 发送普通表单
+ * 发送数据后自动关闭输出流 + * + * @throws IOException + */ + private void sendFormUrlEncoded() throws IOException { + if (StrUtil.isBlank(this.header(Header.CONTENT_TYPE))) { + // 如果未自定义Content-Type,使用默认的application/x-www-form-urlencoded + this.httpConnection.header(Header.CONTENT_TYPE, ContentType.FORM_URLENCODED.toString(this.charset), true); + } + + // Write的时候会优先使用body中的内容,write时自动关闭OutputStream + if (ArrayUtil.isNotEmpty(this.bodyBytes)) { + IoUtil.write(this.httpConnection.getOutputStream(), true, this.bodyBytes); + } else { + final String content = HttpUtil.toParams(this.form, this.charset); + IoUtil.write(this.httpConnection.getOutputStream(), this.charset, true, content); + } + } + + /** + * 发送多组件请求(例如包含文件的表单)
+ * 发送数据后自动关闭输出流 + * + * @throws IOException + */ + private void sendMultipart() throws IOException { + setMultipart();// 设置表单类型为Multipart + + try(OutputStream out = this.httpConnection.getOutputStream()) { + writeFileForm(out); + writeForm(out); + formEnd(out); + } catch (IOException e) { + throw e; + } + } + + // 普通字符串数据 + /** + * 发送普通表单内容 + * + * @param out 输出流 + * @throws IOException + */ + private void writeForm(OutputStream out) throws IOException { + if (CollectionUtil.isNotEmpty(this.form)) { + StringBuilder builder = StrUtil.builder(); + for (Entry entry : this.form.entrySet()) { + builder.append("--").append(BOUNDARY).append(StrUtil.CRLF); + builder.append(StrUtil.format(CONTENT_DISPOSITION_TEMPLATE, entry.getKey())); + builder.append(entry.getValue()).append(StrUtil.CRLF); + } + IoUtil.write(out, this.charset, false, builder); + } + } + + /** + * 发送文件对象表单 + * + * @param out 输出流 + * @throws IOException + */ + private void writeFileForm(OutputStream out) throws IOException { + for (Entry entry : this.fileForm.entrySet()) { + appendPart(entry.getKey(), entry.getValue(), out); + } + } + + /** + * 添加Multipart表单的数据项 + * + * @param formFieldName 表单名 + * @param resource 资源,可以是文件等 + * @param out Http流 + * @since 4.1.0 + */ + private void appendPart(String formFieldName, Resource resource, OutputStream out) { + if (resource instanceof MultiResource) { + // 多资源 + for (Resource subResource : (MultiResource) resource) { + appendPart(formFieldName, subResource, out); + } + } else { + // 普通资源 + final StringBuilder builder = StrUtil.builder().append("--").append(BOUNDARY).append(StrUtil.CRLF); + final String fileName = resource.getName(); + builder.append(StrUtil.format(CONTENT_DISPOSITION_FILE_TEMPLATE, formFieldName, ObjectUtil.defaultIfNull(fileName, formFieldName))); + builder.append(StrUtil.format(CONTENT_TYPE_FILE_TEMPLATE, HttpUtil.getMimeType(fileName))); + IoUtil.write(out, this.charset, false, builder); + InputStream in = null; + try { + in = resource.getStream(); + IoUtil.copy(in, out); + } finally { + IoUtil.close(in); + } + IoUtil.write(out, this.charset, false, StrUtil.CRLF); + } + + } + + // 添加结尾数据 + /** + * 上传表单结束 + * + * @param out 输出流 + * @throws IOException + */ + private void formEnd(OutputStream out) throws IOException { + out.write(BOUNDARY_END); + out.flush(); + } + + /** + * 设置表单类型为Multipart(文件上传) + * + * @return HttpConnection + */ + private void setMultipart() { + this.httpConnection.header(Header.CONTENT_TYPE, CONTENT_TYPE_MULTIPART_PREFIX + BOUNDARY, true); + } + + /** + * 是否忽略读取响应body部分
+ * HEAD、CONNECT、OPTIONS、TRACE方法将不读取响应体 + * + * @return 是否需要忽略响应body部分 + * @since 3.1.2 + */ + private boolean isIgnoreResponseBody() { + if (Method.HEAD == this.method || Method.CONNECT == this.method || Method.OPTIONS == this.method || Method.TRACE == this.method) { + return true; + } + return false; + } + // ---------------------------------------------------------------- Private method end + +} diff --git a/hutool-http/src/main/java/cn/hutool/http/HttpResponse.java b/hutool-http/src/main/java/cn/hutool/http/HttpResponse.java new file mode 100644 index 000000000..a3671ea63 --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/HttpResponse.java @@ -0,0 +1,502 @@ +package cn.hutool.http; + +import java.io.ByteArrayInputStream; +import java.io.Closeable; +import java.io.EOFException; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpCookie; +import java.nio.charset.Charset; +import java.util.List; +import java.util.Map.Entry; +import java.util.zip.DeflaterInputStream; +import java.util.zip.GZIPInputStream; + +import cn.hutool.core.convert.Convert; +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.StreamProgress; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.ReUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; +import cn.hutool.http.cookie.GlobalCookieManager; +import cn.hutool.log.StaticLog; + +/** + * Http响应类
+ * 非线程安全对象 + * + * @author Looly + * + */ +public class HttpResponse extends HttpBase implements Closeable { + + /** 持有连接对象 */ + private HttpConnection httpConnection; + /** Http请求原始流 */ + private InputStream in; + /** 是否异步,异步下只持有流,否则将在初始化时直接读取body内容 */ + private volatile boolean isAsync; + /** 响应状态码 */ + private int status; + /** 是否忽略读取Http响应体 */ + private boolean ignoreBody; + /** 从响应中获取的编码 */ + private Charset charsetFromResponse; + + /** + * 构造 + * + * @param httpConnection {@link HttpConnection} + * @param charset 编码,从请求编码中获取默认编码 + * @param isAsync 是否异步 + * @param isIgnoreBody 是否忽略读取响应体 + * @since 3.1.2 + */ + protected HttpResponse(HttpConnection httpConnection, Charset charset, boolean isAsync, boolean isIgnoreBody) { + this.httpConnection = httpConnection; + this.charset = charset; + this.isAsync = isAsync; + this.ignoreBody = isIgnoreBody; + initWithDisconnect(); + } + + /** + * 获取状态码 + * + * @return 状态码 + */ + public int getStatus() { + return this.status; + } + + /** + * 请求是否成功,判断依据为:状态码范围在200~299内。 + * + * @return 是否成功请求 + * @since 4.1.9 + */ + public boolean isOk() { + return this.status >= 200 && this.status < 300; + } + + /** + * 同步
+ * 如果为异步状态,则暂时不读取服务器中响应的内容,而是持有Http链接的{@link InputStream}。
+ * 当调用此方法时,异步状态转为同步状态,此时从Http链接流中读取body内容并暂存在内容中。如果已经是同步状态,则不进行任何操作。 + * + * @return this + */ + public HttpResponse sync() { + return this.isAsync ? forceSync() : this; + } + + // ---------------------------------------------------------------- Http Response Header start + /** + * 获取内容编码 + * + * @return String + */ + public String contentEncoding() { + return header(Header.CONTENT_ENCODING); + } + + /** + * 是否为gzip压缩过的内容 + * + * @return 是否为gzip压缩过的内容 + */ + public boolean isGzip() { + final String contentEncoding = contentEncoding(); + return contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip"); + } + + /** + * 是否为zlib(Defalte)压缩过的内容 + * + * @return 是否为zlib(Defalte)压缩过的内容 + * @since 4.5.7 + */ + public boolean isDeflate() { + final String contentEncoding = contentEncoding(); + return contentEncoding != null && contentEncoding.equalsIgnoreCase("deflate"); + } + + /** + * 是否为Transfer-Encoding:Chunked的内容 + * + * @return 是否为Transfer-Encoding:Chunked的内容 + * @since 4.6.2 + */ + public boolean isChunked() { + final String transferEncoding = header(Header.TRANSFER_ENCODING); + return transferEncoding != null && transferEncoding.equalsIgnoreCase("Chunked"); + } + + /** + * 获取本次请求服务器返回的Cookie信息 + * + * @return Cookie字符串 + * @since 3.1.1 + */ + public String getCookieStr() { + return header(Header.SET_COOKIE); + } + + /** + * 获取Cookie + * + * @return Cookie列表 + * @since 3.1.1 + * @see GlobalCookieManager#getCookieManager() + */ + public List getCookies() { + return GlobalCookieManager.getCookieManager().getCookieStore().getCookies(); + } + + /** + * 获取Cookie + * + * @param name Cookie名 + * @return {@link HttpCookie} + * @since 4.1.4 + */ + public HttpCookie getCookie(String name) { + List cookie = getCookies(); + if (null != cookie) { + for (HttpCookie httpCookie : cookie) { + if (httpCookie.getName().equals(name)) { + return httpCookie; + } + } + } + return null; + } + + /** + * 获取Cookie值 + * + * @param name Cookie名 + * @return Cookie值 + * @since 4.1.4 + */ + public String getCookieValue(String name) { + HttpCookie cookie = getCookie(name); + return (null == cookie) ? null : cookie.getValue(); + } + // ---------------------------------------------------------------- Http Response Header end + + // ---------------------------------------------------------------- Body start + /** + * 获得服务区响应流
+ * 异步模式下获取Http原生流,同步模式下获取获取到的在内存中的副本
+ * 如果想在同步模式下获取流,请先调用{@link #sync()}方法强制同步
+ * 流获取后处理完毕需关闭此类 + * + * @return 响应流 + */ + public InputStream bodyStream() { + if (isAsync) { + return this.in; + } + return new ByteArrayInputStream(this.bodyBytes); + } + + /** + * 获取响应流字节码
+ * 此方法会转为同步模式 + * + * @return byte[] + */ + public byte[] bodyBytes() { + sync(); + return this.bodyBytes; + } + + /** + * 获取响应主体 + * + * @return String + * @throws HttpException 包装IO异常 + */ + public String body() throws HttpException { + return HttpUtil.getString(bodyBytes(), this.charset, null == this.charsetFromResponse); + } + + /** + * 将响应内容写出到{@link OutputStream}
+ * 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出
+ * 写出后会关闭Http流(异步模式) + * + * @param out 写出的流 + * @param isCloseOut 是否关闭输出流 + * @param streamProgress 进度显示接口,通过实现此接口显示下载进度 + * @return 写出bytes数 + * @since 3.3.2 + */ + public long writeBody(OutputStream out, boolean isCloseOut, StreamProgress streamProgress) { + if (null == out) { + throw new NullPointerException("[out] is null!"); + } + try { + return IoUtil.copyByNIO(bodyStream(), out, IoUtil.DEFAULT_BUFFER_SIZE, streamProgress); + } finally { + IoUtil.close(this); + if (isCloseOut) { + IoUtil.close(out); + } + } + } + + /** + * 将响应内容写出到文件
+ * 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出
+ * 写出后会关闭Http流(异步模式) + * + * @param destFile 写出到的文件 + * @param streamProgress 进度显示接口,通过实现此接口显示下载进度 + * @return 写出bytes数 + * @since 3.3.2 + */ + public long writeBody(File destFile, StreamProgress streamProgress) { + if (null == destFile) { + throw new NullPointerException("[destFile] is null!"); + } + if (destFile.isDirectory()) { + // 从头信息中获取文件名 + String fileName = getFileNameFromDisposition(); + if (StrUtil.isBlank(fileName)) { + final String path = this.httpConnection.getUrl().getPath(); + // 从路径中获取文件名 + fileName = StrUtil.subSuf(path, path.lastIndexOf('/') + 1); + if (StrUtil.isBlank(fileName)) { + // 编码后的路径做为文件名 + fileName = URLUtil.encodeQuery(path, CharsetUtil.CHARSET_UTF_8); + } + } + destFile = FileUtil.file(destFile, fileName); + } + + return writeBody(FileUtil.getOutputStream(destFile), true, streamProgress); + } + + /** + * 将响应内容写出到文件
+ * 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出
+ * 写出后会关闭Http流(异步模式) + * + * @param destFile 写出到的文件 + * @return 写出bytes数 + * @since 3.3.2 + */ + public long writeBody(File destFile) { + return writeBody(destFile, null); + } + + /** + * 将响应内容写出到文件
+ * 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出
+ * 写出后会关闭Http流(异步模式) + * + * @param destFilePath 写出到的文件的路径 + * @return 写出bytes数 + * @since 3.3.2 + */ + public long writeBody(String destFilePath) { + return writeBody(FileUtil.file(destFilePath)); + } + // ---------------------------------------------------------------- Body end + + @Override + public void close() { + IoUtil.close(this.in); + this.in = null; + // 关闭连接 + this.httpConnection.disconnectQuietly(); + } + + @Override + public String toString() { + StringBuilder sb = StrUtil.builder(); + sb.append("Response Headers: ").append(StrUtil.CRLF); + for (Entry> entry : this.headers.entrySet()) { + sb.append(" ").append(entry).append(StrUtil.CRLF); + } + + sb.append("Response Body: ").append(StrUtil.CRLF); + sb.append(" ").append(this.body()).append(StrUtil.CRLF); + + return sb.toString(); + } + + // ---------------------------------------------------------------- Private method start + /** + * 初始化Http响应,并在报错时关闭连接。
+ * 初始化包括: + * + *
+	 * 1、读取Http状态
+	 * 2、读取头信息
+	 * 3、持有Http流,并不关闭流
+	 * 
+ * + * @return this + * @throws HttpException IO异常 + */ + private HttpResponse initWithDisconnect() throws HttpException { + try { + init(); + } catch (HttpException e) { + this.httpConnection.disconnectQuietly(); + throw e; + } + return this; + } + + /** + * 初始化Http响应
+ * 初始化包括: + * + *
+	 * 1、读取Http状态
+	 * 2、读取头信息
+	 * 3、持有Http流,并不关闭流
+	 * 
+ * + * @return this + * @throws HttpException IO异常 + */ + private HttpResponse init() throws HttpException { + try { + this.status = httpConnection.responseCode(); + this.in = (this.status < HttpStatus.HTTP_BAD_REQUEST) ? this.httpConnection.getInputStream() : this.httpConnection.getErrorStream(); + } catch (IOException e) { + if (e instanceof FileNotFoundException) { + // 服务器无返回内容,忽略之 + } else { + throw new HttpException(e); + } + } + + // 读取响应头信息 + try { + this.headers = httpConnection.headers(); + } catch (IllegalArgumentException e) { + StaticLog.warn(e, e.getMessage()); + } + + // 存储服务端设置的Cookie信息 + GlobalCookieManager.store(httpConnection); + + final Charset charset = httpConnection.getCharset(); + this.charsetFromResponse = charset; + if (null != charset) { + this.charset = charset; + } + + if (null == this.in) { + // 在一些情况下,返回的流为null,此时提供状态码说明 + this.in = new ByteArrayInputStream(StrUtil.format("Error request, response status: {}", this.status).getBytes()); + } else { + // TODO 分段响应内容解析 + + if (isGzip() && false == (in instanceof GZIPInputStream)) { + // Accept-Encoding: gzip + try { + in = new GZIPInputStream(in); + } catch (IOException e) { + // 在类似于Head等方法中无body返回,此时GZIPInputStream构造会出现错误,在此忽略此错误读取普通数据 + // ignore + } + } else if (isDeflate() && false == (in instanceof DeflaterInputStream)) { + // Accept-Encoding: defalte + in = new DeflaterInputStream(in); + } + } + + // 同步情况下强制同步 + return this.isAsync ? this : forceSync(); + } + + /** + * 读取主体,忽略EOFException异常 + * + * @param in 输入流 + * @return 自身 + * @throws IORuntimeException IO异常 + */ + private void readBody(InputStream in) throws IORuntimeException { + if (ignoreBody) { + return; + } + + int contentLength = Convert.toInt(header(Header.CONTENT_LENGTH), 0); + final FastByteArrayOutputStream out = contentLength > 0 ? new FastByteArrayOutputStream(contentLength) : new FastByteArrayOutputStream(); + try { + IoUtil.copy(in, out); + } catch (IORuntimeException e) { + if (e.getCause() instanceof EOFException || StrUtil.containsIgnoreCase(e.getMessage(), "Premature EOF")) { + // 忽略读取HTTP流中的EOF错误 + } else { + throw e; + } + } + this.bodyBytes = out.toByteArray(); + } + + /** + * 强制同步,用于初始化
+ * 强制同步后变化如下: + * + *
+	 * 1、读取body内容到内存
+	 * 2、异步状态设为false(变为同步状态)
+	 * 3、关闭Http流
+	 * 4、断开与服务器连接
+	 * 
+ * + * @return this + */ + private HttpResponse forceSync() { + // 非同步状态转为同步状态 + try { + this.readBody(this.in); + } catch (IORuntimeException e) { + if (e.getCause() instanceof FileNotFoundException) { + // 服务器无返回内容,忽略之 + } else { + throw new HttpException(e); + } + } finally { + if (this.isAsync) { + this.isAsync = false; + } + this.close(); + } + return this; + } + + /** + * 从Content-Disposition头中获取文件名 + * + * @return 文件名,empty表示无 + */ + private String getFileNameFromDisposition() { + String fileName = null; + final String desposition = header(Header.CONTENT_DISPOSITION); + if (StrUtil.isNotBlank(desposition)) { + fileName = ReUtil.get("filename=\"(.*?)\"", desposition, 1); + if (StrUtil.isBlank(fileName)) { + fileName = StrUtil.subAfter(desposition, "filename=", true); + } + } + return fileName; + } + // ---------------------------------------------------------------- Private method end +} diff --git a/hutool-http/src/main/java/cn/hutool/http/HttpStatus.java b/hutool-http/src/main/java/cn/hutool/http/HttpStatus.java new file mode 100644 index 000000000..566209522 --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/HttpStatus.java @@ -0,0 +1,194 @@ +package cn.hutool.http; + +/** + * HTTP状态码 + * + * @author Looly + * @see java.net.HttpURLConnection + * + */ +public class HttpStatus { + + /* 2XX: generally "OK" */ + + /** + * HTTP Status-Code 200: OK. + */ + public static final int HTTP_OK = 200; + + /** + * HTTP Status-Code 201: Created. + */ + public static final int HTTP_CREATED = 201; + + /** + * HTTP Status-Code 202: Accepted. + */ + public static final int HTTP_ACCEPTED = 202; + + /** + * HTTP Status-Code 203: Non-Authoritative Information. + */ + public static final int HTTP_NOT_AUTHORITATIVE = 203; + + /** + * HTTP Status-Code 204: No Content. + */ + public static final int HTTP_NO_CONTENT = 204; + + /** + * HTTP Status-Code 205: Reset Content. + */ + public static final int HTTP_RESET = 205; + + /** + * HTTP Status-Code 206: Partial Content. + */ + public static final int HTTP_PARTIAL = 206; + + /* 3XX: relocation/redirect */ + + /** + * HTTP Status-Code 300: Multiple Choices. + */ + public static final int HTTP_MULT_CHOICE = 300; + + /** + * HTTP Status-Code 301: Moved Permanently. + */ + public static final int HTTP_MOVED_PERM = 301; + + /** + * HTTP Status-Code 302: Temporary Redirect. + */ + public static final int HTTP_MOVED_TEMP = 302; + + /** + * HTTP Status-Code 303: See Other. + */ + public static final int HTTP_SEE_OTHER = 303; + + /** + * HTTP Status-Code 304: Not Modified. + */ + public static final int HTTP_NOT_MODIFIED = 304; + + /** + * HTTP Status-Code 305: Use Proxy. + */ + public static final int HTTP_USE_PROXY = 305; + + /* 4XX: client error */ + + /** + * HTTP Status-Code 400: Bad Request. + */ + public static final int HTTP_BAD_REQUEST = 400; + + /** + * HTTP Status-Code 401: Unauthorized. + */ + public static final int HTTP_UNAUTHORIZED = 401; + + /** + * HTTP Status-Code 402: Payment Required. + */ + public static final int HTTP_PAYMENT_REQUIRED = 402; + + /** + * HTTP Status-Code 403: Forbidden. + */ + public static final int HTTP_FORBIDDEN = 403; + + /** + * HTTP Status-Code 404: Not Found. + */ + public static final int HTTP_NOT_FOUND = 404; + + /** + * HTTP Status-Code 405: Method Not Allowed. + */ + public static final int HTTP_BAD_METHOD = 405; + + /** + * HTTP Status-Code 406: Not Acceptable. + */ + public static final int HTTP_NOT_ACCEPTABLE = 406; + + /** + * HTTP Status-Code 407: Proxy Authentication Required. + */ + public static final int HTTP_PROXY_AUTH = 407; + + /** + * HTTP Status-Code 408: Request Time-Out. + */ + public static final int HTTP_CLIENT_TIMEOUT = 408; + + /** + * HTTP Status-Code 409: Conflict. + */ + public static final int HTTP_CONFLICT = 409; + + /** + * HTTP Status-Code 410: Gone. + */ + public static final int HTTP_GONE = 410; + + /** + * HTTP Status-Code 411: Length Required. + */ + public static final int HTTP_LENGTH_REQUIRED = 411; + + /** + * HTTP Status-Code 412: Precondition Failed. + */ + public static final int HTTP_PRECON_FAILED = 412; + + /** + * HTTP Status-Code 413: Request Entity Too Large. + */ + public static final int HTTP_ENTITY_TOO_LARGE = 413; + + /** + * HTTP Status-Code 414: Request-URI Too Large. + */ + public static final int HTTP_REQ_TOO_LONG = 414; + + /** + * HTTP Status-Code 415: Unsupported Media Type. + */ + public static final int HTTP_UNSUPPORTED_TYPE = 415; + + /* 5XX: server error */ + + /** + * HTTP Status-Code 500: Internal Server Error. + */ + public static final int HTTP_INTERNAL_ERROR = 500; + + /** + * HTTP Status-Code 501: Not Implemented. + */ + public static final int HTTP_NOT_IMPLEMENTED = 501; + + /** + * HTTP Status-Code 502: Bad Gateway. + */ + public static final int HTTP_BAD_GATEWAY = 502; + + /** + * HTTP Status-Code 503: Service Unavailable. + */ + public static final int HTTP_UNAVAILABLE = 503; + + /** + * HTTP Status-Code 504: Gateway Timeout. + */ + public static final int HTTP_GATEWAY_TIMEOUT = 504; + + /** + * HTTP Status-Code 505: HTTP Version Not Supported. + */ + public static final int HTTP_VERSION = 505; +} diff --git a/hutool-http/src/main/java/cn/hutool/http/HttpUtil.java b/hutool-http/src/main/java/cn/hutool/http/HttpUtil.java new file mode 100644 index 000000000..63d807063 --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/HttpUtil.java @@ -0,0 +1,780 @@ +package cn.hutool.http; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.regex.Pattern; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.io.FastByteArrayOutputStream; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.io.StreamProgress; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.text.StrBuilder; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.ReUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; + +/** + * Http请求工具类 + * + * @author xiaoleilu + */ +public class HttpUtil { + + /** 正则:Content-Type中的编码信息 */ + public static final Pattern CHARSET_PATTERN = Pattern.compile("charset\\s*=\\s*([a-z0-9-]*)", Pattern.CASE_INSENSITIVE); + /** 正则:匹配meta标签的编码信息 */ + public static final Pattern META_CHARSET_PATTERN = Pattern.compile("]*?charset\\s*=\\s*['\"]?([a-z0-9-]*)", Pattern.CASE_INSENSITIVE); + + /** + * 检测是否https + * + * @param url URL + * @return 是否https + */ + public static boolean isHttps(String url) { + return url.toLowerCase().startsWith("https"); + } + + /** + * 创建Http请求对象 + * + * @param method 方法枚举{@link Method} + * @param url 请求的URL,可以使HTTP或者HTTPS + * @return {@link HttpRequest} + * @since 3.0.9 + */ + public static HttpRequest createRequest(Method method, String url) { + return new HttpRequest(url).method(method); + } + + /** + * 创建Http GET请求对象 + * + * @param url 请求的URL,可以使HTTP或者HTTPS + * @return {@link HttpRequest} + * @since 3.2.0 + */ + public static HttpRequest createGet(String url) { + return HttpRequest.get(url); + } + + /** + * 创建Http POST请求对象 + * + * @param url 请求的URL,可以使HTTP或者HTTPS + * @return {@link HttpRequest} + * @since 3.2.0 + */ + public static HttpRequest createPost(String url) { + return HttpRequest.post(url); + } + + /** + * 发送get请求 + * + * @param urlString 网址 + * @param customCharset 自定义请求字符集,如果字符集获取不到,使用此字符集 + * @return 返回内容,如果只检查状态码,正常只返回 "",不正常返回 null + */ + public static String get(String urlString, Charset customCharset) { + return HttpRequest.get(urlString).charset(customCharset).execute().body(); + } + + /** + * 发送get请求 + * + * @param urlString 网址 + * @return 返回内容,如果只检查状态码,正常只返回 "",不正常返回 null + */ + public static String get(String urlString) { + return get(urlString, HttpGlobalConfig.timeout); + } + + /** + * 发送get请求 + * + * @param urlString 网址 + * @param timeout 超时时长,-1表示默认超时,单位毫秒 + * @return 返回内容,如果只检查状态码,正常只返回 "",不正常返回 null + * @since 3.2.0 + */ + public static String get(String urlString, int timeout) { + return HttpRequest.get(urlString).timeout(timeout).execute().body(); + } + + /** + * 发送get请求 + * + * @param urlString 网址 + * @param paramMap post表单数据 + * @return 返回数据 + */ + public static String get(String urlString, Map paramMap) { + return HttpRequest.get(urlString).form(paramMap).execute().body(); + } + + /** + * 发送get请求 + * + * @param urlString 网址 + * @param paramMap post表单数据 + * @param timeout 超时时长,-1表示默认超时,单位毫秒 + * @return 返回数据 + * @since 3.3.0 + */ + public static String get(String urlString, Map paramMap, int timeout) { + return HttpRequest.get(urlString).form(paramMap).timeout(timeout).execute().body(); + } + + /** + * 发送post请求 + * + * @param urlString 网址 + * @param paramMap post表单数据 + * @return 返回数据 + */ + public static String post(String urlString, Map paramMap) { + return post(urlString, paramMap, HttpGlobalConfig.timeout); + } + + /** + * 发送post请求 + * + * @param urlString 网址 + * @param paramMap post表单数据 + * @param timeout 超时时长,-1表示默认超时,单位毫秒 + * @return 返回数据 + * @since 3.2.0 + */ + public static String post(String urlString, Map paramMap, int timeout) { + return HttpRequest.post(urlString).form(paramMap).timeout(timeout).execute().body(); + } + + /** + * 发送post请求
+ * 请求体body参数支持两种类型: + * + *
+	 * 1. 标准参数,例如 a=1&b=2 这种格式
+	 * 2. Rest模式,此时body需要传入一个JSON或者XML字符串,Hutool会自动绑定其对应的Content-Type
+	 * 
+ * + * @param urlString 网址 + * @param body post表单数据 + * @return 返回数据 + */ + public static String post(String urlString, String body) { + return post(urlString, body, HttpGlobalConfig.timeout); + } + + /** + * 发送post请求
+ * 请求体body参数支持两种类型: + * + *
+	 * 1. 标准参数,例如 a=1&b=2 这种格式
+	 * 2. Rest模式,此时body需要传入一个JSON或者XML字符串,Hutool会自动绑定其对应的Content-Type
+	 * 
+ * + * @param urlString 网址 + * @param body post表单数据 + * @param timeout 超时时长,-1表示默认超时,单位毫秒 + * @return 返回数据 + * @since 3.2.0 + */ + public static String post(String urlString, String body, int timeout) { + return HttpRequest.post(urlString).timeout(timeout).body(body).execute().body(); + } + + // ---------------------------------------------------------------------------------------- download + /** + * 下载远程文本 + * + * @param url 请求的url + * @param customCharsetName 自定义的字符集 + * @return 文本 + */ + public static String downloadString(String url, String customCharsetName) { + return downloadString(url, CharsetUtil.charset(customCharsetName), null); + } + + /** + * 下载远程文本 + * + * @param url 请求的url + * @param customCharset 自定义的字符集,可以使用{@link CharsetUtil#charset} 方法转换 + * @return 文本 + */ + public static String downloadString(String url, Charset customCharset) { + return downloadString(url, customCharset, null); + } + + /** + * 下载远程文本 + * + * @param url 请求的url + * @param customCharset 自定义的字符集,可以使用{@link CharsetUtil#charset} 方法转换 + * @param streamPress 进度条 {@link StreamProgress} + * @return 文本 + */ + public static String downloadString(String url, Charset customCharset, StreamProgress streamPress) { + if (StrUtil.isBlank(url)) { + throw new NullPointerException("[url] is null!"); + } + + FastByteArrayOutputStream out = new FastByteArrayOutputStream(); + download(url, out, true, streamPress); + return null == customCharset ? out.toString() : out.toString(customCharset); + } + + /** + * 下载远程文件 + * + * @param url 请求的url + * @param dest 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名 + * @return 文件大小 + */ + public static long downloadFile(String url, String dest) { + return downloadFile(url, FileUtil.file(dest)); + } + + /** + * 下载远程文件 + * + * @param url 请求的url + * @param destFile 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名 + * @return 文件大小 + */ + public static long downloadFile(String url, File destFile) { + return downloadFile(url, destFile, null); + } + + /** + * 下载远程文件 + * + * @param url 请求的url + * @param destFile 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名 + * @param timeout 超时,单位毫秒,-1表示默认超时 + * @return 文件大小 + * @since 4.0.4 + */ + public static long downloadFile(String url, File destFile, int timeout) { + return downloadFile(url, destFile, timeout, null); + } + + /** + * 下载远程文件 + * + * @param url 请求的url + * @param destFile 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名 + * @param streamProgress 进度条 + * @return 文件大小 + */ + public static long downloadFile(String url, File destFile, StreamProgress streamProgress) { + return downloadFile(url, destFile, -1, streamProgress); + } + + /** + * 下载远程文件 + * + * @param url 请求的url + * @param destFile 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名 + * @param timeout 超时,单位毫秒,-1表示默认超时 + * @param streamProgress 进度条 + * @return 文件大小 + * @since 4.0.4 + */ + public static long downloadFile(String url, File destFile, int timeout, StreamProgress streamProgress) { + if (StrUtil.isBlank(url)) { + throw new NullPointerException("[url] is null!"); + } + if (null == destFile) { + throw new NullPointerException("[destFile] is null!"); + } + final HttpResponse response = HttpRequest.get(url).timeout(timeout).executeAsync(); + if (false == response.isOk()) { + throw new HttpException("Server response error with status code: [{}]", response.getStatus()); + } + return response.writeBody(destFile, streamProgress); + } + + /** + * 下载远程文件 + * + * @param url 请求的url + * @param out 将下载内容写到输出流中 {@link OutputStream} + * @param isCloseOut 是否关闭输出流 + * @return 文件大小 + */ + public static long download(String url, OutputStream out, boolean isCloseOut) { + return download(url, out, isCloseOut, null); + } + + /** + * 下载远程文件 + * + * @param url 请求的url + * @param out 将下载内容写到输出流中 {@link OutputStream} + * @param isCloseOut 是否关闭输出流 + * @param streamProgress 进度条 + * @return 文件大小 + */ + public static long download(String url, OutputStream out, boolean isCloseOut, StreamProgress streamProgress) { + if (StrUtil.isBlank(url)) { + throw new NullPointerException("[url] is null!"); + } + if (null == out) { + throw new NullPointerException("[out] is null!"); + } + + final HttpResponse response = HttpRequest.get(url).executeAsync(); + if (false == response.isOk()) { + throw new HttpException("Server response error with status code: [{}]", response.getStatus()); + } + return response.writeBody(out, isCloseOut, streamProgress); + } + + /** + * 将Map形式的Form表单数据转换为Url参数形式,不做编码 + * + * @param paramMap 表单数据 + * @return url参数 + */ + public static String toParams(Map paramMap) { + return toParams(paramMap, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 将Map形式的Form表单数据转换为Url参数形式
+ * 编码键和值对 + * + * @param paramMap 表单数据 + * @param charsetName 编码 + * @return url参数 + */ + public static String toParams(Map paramMap, String charsetName) { + return toParams(paramMap, CharsetUtil.charset(charsetName)); + } + + /** + * 将Map形式的Form表单数据转换为Url参数形式
+ * paramMap中如果key为空(null和"")会被忽略,如果value为null,会被做为空白符("")
+ * 会自动url编码键和值 + * + *
+	 * key1=v1&key2=&key3=v3
+	 * 
+ * + * @param paramMap 表单数据 + * @param charset 编码 + * @return url参数 + */ + public static String toParams(Map paramMap, Charset charset) { + if (CollectionUtil.isEmpty(paramMap)) { + return StrUtil.EMPTY; + } + if (null == charset) {// 默认编码为系统编码 + charset = CharsetUtil.CHARSET_UTF_8; + } + + final StringBuilder sb = new StringBuilder(); + boolean isFirst = true; + String key; + Object value; + String valueStr; + for (Entry item : paramMap.entrySet()) { + if (isFirst) { + isFirst = false; + } else { + sb.append("&"); + } + key = item.getKey(); + value = item.getValue(); + if (value instanceof Iterable) { + value = CollectionUtil.join((Iterable) value, ","); + } else if (value instanceof Iterator) { + value = CollectionUtil.join((Iterator) value, ","); + } + valueStr = Convert.toStr(value); + if (StrUtil.isNotEmpty(key)) { + sb.append(URLUtil.encodeAll(key, charset)).append("="); + if (StrUtil.isNotEmpty(valueStr)) { + sb.append(URLUtil.encodeAll(valueStr, charset)); + } + } + } + return sb.toString(); + } + + /** + * 对URL参数做编码,只编码键和值
+ * 提供的值可以是url附带参数,但是不能只是url + * + *

注意,此方法只能标准化整个URL,并不适合于单独编码参数值

+ * + * + * @param paramsStr url参数,可以包含url本身 + * @param charset 编码 + * @return 编码后的url和参数 + * @since 4.0.1 + */ + public static String encodeParams(String paramsStr, Charset charset) { + if (StrUtil.isBlank(paramsStr)) { + return StrUtil.EMPTY; + } + + String urlPart = null; // url部分,不包括问号 + String paramPart; // 参数部分 + int pathEndPos = paramsStr.indexOf('?'); + if (pathEndPos > -1) { + // url + 参数 + urlPart = StrUtil.subPre(paramsStr, pathEndPos); + paramPart = StrUtil.subSuf(paramsStr, pathEndPos + 1); + if (StrUtil.isBlank(paramPart)) { + // 无参数,返回url + return urlPart; + } + } else { + // 无URL + paramPart = paramsStr; + } + + paramPart = normalizeParams(paramPart, charset); + + return StrUtil.isBlank(urlPart) ? paramPart : urlPart + "?" + paramPart; + } + + /** + * 标准化参数字符串,即URL中?后的部分 + * + *

注意,此方法只能标准化整个URL,并不适合于单独编码参数值

+ * + * @param paramPart 参数字符串 + * @param charset 编码 + * @return 标准化的参数字符串 + * @since 4.5.2 + */ + public static String normalizeParams(String paramPart, Charset charset) { + final StrBuilder builder = StrBuilder.create(paramPart.length() + 16); + final int len = paramPart.length(); + String name = null; + int pos = 0; // 未处理字符开始位置 + char c; // 当前字符 + int i; // 当前字符位置 + for (i = 0; i < len; i++) { + c = paramPart.charAt(i); + if (c == '=') { // 键值对的分界点 + if (null == name) { + // 只有=前未定义name时被当作键值分界符,否则做为普通字符 + name = (pos == i) ? StrUtil.EMPTY : paramPart.substring(pos, i); + pos = i + 1; + } + } else if (c == '&') { // 参数对的分界点 + if (pos != i) { + if (null == name) { + // 对于像&a&这类无参数值的字符串,我们将name为a的值设为"" + name = paramPart.substring(pos, i); + builder.append(URLUtil.encodeQuery(name, charset)).append('='); + } else { + builder.append(URLUtil.encodeQuery(name, charset)).append('=').append(URLUtil.encodeQuery(paramPart.substring(pos, i), charset)).append('&'); + } + name = null; + } + pos = i + 1; + } + } + + // 结尾处理 + if (null != name) { + builder.append(URLUtil.encodeQuery(name, charset)).append('='); + } + if (pos != i) { + if (null == name && pos > 0) { + builder.append('='); + } + builder.append(URLUtil.encodeQuery(paramPart.substring(pos, i), charset)); + } + + // 以&结尾则去除之 + int lastIndex = builder.length() - 1; + if ('&' == builder.charAt(lastIndex)) { + builder.delTo(lastIndex); + } + return builder.toString(); + } + + /** + * 将URL参数解析为Map(也可以解析Post中的键值对参数) + * + * @param paramsStr 参数字符串(或者带参数的Path) + * @param charset 字符集 + * @return 参数Map + * @since 4.0.2 + */ + public static HashMap decodeParamMap(String paramsStr, String charset) { + final Map> paramsMap = decodeParams(paramsStr, charset); + final HashMap result = MapUtil.newHashMap(paramsMap.size()); + List valueList; + for (Entry> entry : paramsMap.entrySet()) { + valueList = entry.getValue(); + result.put(entry.getKey(), CollUtil.isEmpty(valueList) ? null : valueList.get(0)); + } + return result; + } + + /** + * 将URL参数解析为Map(也可以解析Post中的键值对参数) + * + * @param paramsStr 参数字符串(或者带参数的Path) + * @param charset 字符集 + * @return 参数Map + */ + public static Map> decodeParams(String paramsStr, String charset) { + if (StrUtil.isBlank(paramsStr)) { + return Collections.emptyMap(); + } + + // 去掉Path部分 + int pathEndPos = paramsStr.indexOf('?'); + if (pathEndPos > -1) { + paramsStr = StrUtil.subSuf(paramsStr, pathEndPos + 1); + } + + final Map> params = new LinkedHashMap>(); + final int len = paramsStr.length(); + String name = null; + int pos = 0; // 未处理字符开始位置 + int i; // 未处理字符结束位置 + char c; // 当前字符 + for (i = 0; i < len; i++) { + c = paramsStr.charAt(i); + if (c == '=') { // 键值对的分界点 + if (null == name) { + // name可以是"" + name = paramsStr.substring(pos, i); + } + pos = i + 1; + } else if (c == '&') { // 参数对的分界点 + if (null == name && pos != i) { + // 对于像&a&这类无参数值的字符串,我们将name为a的值设为"" + addParam(params, paramsStr.substring(pos, i), StrUtil.EMPTY, charset); + } else if (name != null) { + addParam(params, name, paramsStr.substring(pos, i), charset); + name = null; + } + pos = i + 1; + } + } + + // 处理结尾 + if (pos != i) { + if (name == null) { + addParam(params, paramsStr.substring(pos, i), StrUtil.EMPTY, charset); + } else { + addParam(params, name, paramsStr.substring(pos, i), charset); + } + } else if (name != null) { + addParam(params, name, StrUtil.EMPTY, charset); + } + + return params; + } + + /** + * 将表单数据加到URL中(用于GET表单提交)
+ * 表单的键值对会被url编码,但是url中原参数不会被编码 + * + * @param url URL + * @param form 表单数据 + * @param charset 编码 + * @param isEncodeParams 是否对键和值做转义处理 + * @return 合成后的URL + */ + public static String urlWithForm(String url, Map form, Charset charset, boolean isEncodeParams) { + if (isEncodeParams && StrUtil.contains(url, '?')) { + // 在需要编码的情况下,如果url中已经有部分参数,则编码之 + url = encodeParams(url, charset); + } + + // url和参数是分别编码的 + return urlWithForm(url, toParams(form, charset), charset, false); + } + + /** + * 将表单数据字符串加到URL中(用于GET表单提交) + * + * @param url URL + * @param queryString 表单数据字符串 + * @param charset 编码 + * @param isEncode 是否对键和值做转义处理 + * @return 拼接后的字符串 + */ + public static String urlWithForm(String url, String queryString, Charset charset, boolean isEncode) { + if (StrUtil.isBlank(queryString)) { + // 无额外参数 + if (StrUtil.contains(url, '?')) { + // url中包含参数 + return isEncode ? encodeParams(url, charset) : url; + } + return url; + } + + // 始终有参数 + final StrBuilder urlBuilder = StrBuilder.create(url.length() + queryString.length() + 16); + int qmIndex = url.indexOf('?'); + if (qmIndex > 0) { + // 原URL带参数,则对这部分参数单独编码(如果选项为进行编码) + urlBuilder.append(isEncode ? encodeParams(url, charset) : url); + if (false == StrUtil.endWith(url, '&')) { + // 已经带参数的情况下追加参数 + urlBuilder.append('&'); + } + } else { + // 原url无参数,则不做编码 + urlBuilder.append(url); + if (qmIndex < 0) { + // 无 '?' 追加之 + urlBuilder.append('?'); + } + } + urlBuilder.append(isEncode ? encodeParams(queryString, charset) : queryString); + return urlBuilder.toString(); + } + + /** + * 从Http连接的头信息中获得字符集
+ * 从ContentType中获取 + * + * @param conn HTTP连接对象 + * @return 字符集 + */ + public static String getCharset(HttpURLConnection conn) { + if (conn == null) { + return null; + } + return ReUtil.get(CHARSET_PATTERN, conn.getContentType(), 1); + } + + /** + * 从流中读取内容
+ * 首先尝试使用charset编码读取内容(如果为空默认UTF-8),如果isGetCharsetFromContent为true,则通过正则在正文中获取编码信息,转换为指定编码; + * + * @param in 输入流 + * @param charset 字符集 + * @param isGetCharsetFromContent 是否从返回内容中获得编码信息 + * @return 内容 + * @throws IOException IO异常 + */ + public static String getString(InputStream in, Charset charset, boolean isGetCharsetFromContent) throws IOException { + final byte[] contentBytes = IoUtil.readBytes(in); + return getString(contentBytes, charset, isGetCharsetFromContent); + } + + /** + * 从流中读取内容
+ * 首先尝试使用charset编码读取内容(如果为空默认UTF-8),如果isGetCharsetFromContent为true,则通过正则在正文中获取编码信息,转换为指定编码; + * + * @param contentBytes 内容byte数组 + * @param charset 字符集 + * @param isGetCharsetFromContent 是否从返回内容中获得编码信息 + * @return 内容 + */ + public static String getString(byte[] contentBytes, Charset charset, boolean isGetCharsetFromContent) { + if (null == contentBytes) { + return null; + } + + if (null == charset) { + charset = CharsetUtil.CHARSET_UTF_8; + } + String content = new String(contentBytes, charset); + if (isGetCharsetFromContent) { + final String charsetInContentStr = ReUtil.get(META_CHARSET_PATTERN, content, 1); + if (StrUtil.isNotBlank(charsetInContentStr)) { + Charset charsetInContent = null; + try { + charsetInContent = Charset.forName(charsetInContentStr); + } catch (Exception e) { + if (StrUtil.containsIgnoreCase(charsetInContentStr, "utf-8") || StrUtil.containsIgnoreCase(charsetInContentStr, "utf8")) { + charsetInContent = CharsetUtil.CHARSET_UTF_8; + } else if (StrUtil.containsIgnoreCase(charsetInContentStr, "gbk")) { + charsetInContent = CharsetUtil.CHARSET_GBK; + } + // ignore + } + if (null != charsetInContent && false == charset.equals(charsetInContent)) { + content = new String(contentBytes, charsetInContent); + } + } + } + return content; + } + + /** + * 根据文件扩展名获得MimeType + * + * @param filePath 文件路径或文件名 + * @return MimeType + * @see FileUtil#getMimeType(String) + */ + public static String getMimeType(String filePath) { + return FileUtil.getMimeType(filePath); + } + + /** + * 从请求参数的body中判断请求的Content-Type类型,支持的类型有: + * + *
+	 * 1. application/json
+	 * 1. application/xml
+	 * 
+ * + * @param body 请求参数体 + * @return Content-Type类型,如果无法判断返回null + * @since 3.2.0 + * @see ContentType#get(String) + */ + public static String getContentTypeByRequestBody(String body) { + final ContentType contentType = ContentType.get(body); + return (null == contentType) ? null : contentType.toString(); + } + // ----------------------------------------------------------------------------------------- Private method start + + /** + * 将键值对加入到值为List类型的Map中 + * + * @param params 参数 + * @param name key + * @param value value + * @param charset 编码 + */ + private static void addParam(Map> params, String name, String value, String charset) { + name = URLUtil.decode(name, charset); + value = URLUtil.decode(value, charset); + List values = params.get(name); + if (values == null) { + values = new ArrayList(1); // 一般是一个参数 + params.put(name, values); + } + values.add(value); + } + + // ----------------------------------------------------------------------------------------- Private method start end +} diff --git a/hutool-http/src/main/java/cn/hutool/http/Method.java b/hutool-http/src/main/java/cn/hutool/http/Method.java new file mode 100644 index 000000000..27886b595 --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/Method.java @@ -0,0 +1,10 @@ +package cn.hutool.http; + +/** + * Http方法枚举 + * @author Looly + * + */ +public enum Method { + GET, POST, HEAD, OPTIONS, PUT, DELETE, TRACE, CONNECT, PATCH; +} diff --git a/hutool-http/src/main/java/cn/hutool/http/Status.java b/hutool-http/src/main/java/cn/hutool/http/Status.java new file mode 100644 index 000000000..d5619a056 --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/Status.java @@ -0,0 +1,189 @@ +package cn.hutool.http; + +/** + * 返回状态码 + * @author Looly + * + */ +public interface Status { + /** + * HTTP Status-Code 200: OK. + */ + public static final int HTTP_OK = 200; + + /** + * HTTP Status-Code 201: Created. + */ + public static final int HTTP_CREATED = 201; + + /** + * HTTP Status-Code 202: Accepted. + */ + public static final int HTTP_ACCEPTED = 202; + + /** + * HTTP Status-Code 203: Non-Authoritative Information. + */ + public static final int HTTP_NOT_AUTHORITATIVE = 203; + + /** + * HTTP Status-Code 204: No Content. + */ + public static final int HTTP_NO_CONTENT = 204; + + /** + * HTTP Status-Code 205: Reset Content. + */ + public static final int HTTP_RESET = 205; + + /** + * HTTP Status-Code 206: Partial Content. + */ + public static final int HTTP_PARTIAL = 206; + + /* 3XX: relocation/redirect */ + + /** + * HTTP Status-Code 300: Multiple Choices. + */ + public static final int HTTP_MULT_CHOICE = 300; + + /** + * HTTP Status-Code 301: Moved Permanently. + */ + public static final int HTTP_MOVED_PERM = 301; + + /** + * HTTP Status-Code 302: Temporary Redirect. + */ + public static final int HTTP_MOVED_TEMP = 302; + + /** + * HTTP Status-Code 303: See Other. + */ + public static final int HTTP_SEE_OTHER = 303; + + /** + * HTTP Status-Code 304: Not Modified. + */ + public static final int HTTP_NOT_MODIFIED = 304; + + /** + * HTTP Status-Code 305: Use Proxy. + */ + public static final int HTTP_USE_PROXY = 305; + + /* 4XX: client error */ + + /** + * HTTP Status-Code 400: Bad Request. + */ + public static final int HTTP_BAD_REQUEST = 400; + + /** + * HTTP Status-Code 401: Unauthorized. + */ + public static final int HTTP_UNAUTHORIZED = 401; + + /** + * HTTP Status-Code 402: Payment Required. + */ + public static final int HTTP_PAYMENT_REQUIRED = 402; + + /** + * HTTP Status-Code 403: Forbidden. + */ + public static final int HTTP_FORBIDDEN = 403; + + /** + * HTTP Status-Code 404: Not Found. + */ + public static final int HTTP_NOT_FOUND = 404; + + /** + * HTTP Status-Code 405: Method Not Allowed. + */ + public static final int HTTP_BAD_METHOD = 405; + + /** + * HTTP Status-Code 406: Not Acceptable. + */ + public static final int HTTP_NOT_ACCEPTABLE = 406; + + /** + * HTTP Status-Code 407: Proxy Authentication Required. + */ + public static final int HTTP_PROXY_AUTH = 407; + + /** + * HTTP Status-Code 408: Request Time-Out. + */ + public static final int HTTP_CLIENT_TIMEOUT = 408; + + /** + * HTTP Status-Code 409: Conflict. + */ + public static final int HTTP_CONFLICT = 409; + + /** + * HTTP Status-Code 410: Gone. + */ + public static final int HTTP_GONE = 410; + + /** + * HTTP Status-Code 411: Length Required. + */ + public static final int HTTP_LENGTH_REQUIRED = 411; + + /** + * HTTP Status-Code 412: Precondition Failed. + */ + public static final int HTTP_PRECON_FAILED = 412; + + /** + * HTTP Status-Code 413: Request Entity Too Large. + */ + public static final int HTTP_ENTITY_TOO_LARGE = 413; + + /** + * HTTP Status-Code 414: Request-URI Too Large. + */ + public static final int HTTP_REQ_TOO_LONG = 414; + + /** + * HTTP Status-Code 415: Unsupported Media Type. + */ + public static final int HTTP_UNSUPPORTED_TYPE = 415; + + /* 5XX: server error */ + + /** + * HTTP Status-Code 500: Internal Server Error. + */ + public static final int HTTP_INTERNAL_ERROR = 500; + + /** + * HTTP Status-Code 501: Not Implemented. + */ + public static final int HTTP_NOT_IMPLEMENTED = 501; + + /** + * HTTP Status-Code 502: Bad Gateway. + */ + public static final int HTTP_BAD_GATEWAY = 502; + + /** + * HTTP Status-Code 503: Service Unavailable. + */ + public static final int HTTP_UNAVAILABLE = 503; + + /** + * HTTP Status-Code 504: Gateway Timeout. + */ + public static final int HTTP_GATEWAY_TIMEOUT = 504; + + /** + * HTTP Status-Code 505: HTTP Version Not Supported. + */ + public static final int HTTP_VERSION = 505; +} diff --git a/hutool-http/src/main/java/cn/hutool/http/cookie/GlobalCookieManager.java b/hutool-http/src/main/java/cn/hutool/http/cookie/GlobalCookieManager.java new file mode 100644 index 000000000..e3f0b322f --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/cookie/GlobalCookieManager.java @@ -0,0 +1,102 @@ +package cn.hutool.http.cookie; + +import java.io.IOException; +import java.net.CookieManager; +import java.net.CookiePolicy; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; +import cn.hutool.http.Header; +import cn.hutool.http.HttpConnection; +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; + +/** + * 全局Cooki管理器,只针对Hutool请求有效 + * + * @author Looly + * @since 4.5.15 + */ +public class GlobalCookieManager { + private static Log log = LogFactory.get(); + + /** Cookie管理 */ + private static CookieManager cookieManager; + static { + cookieManager = new CookieManager(new ThreadLocalCookieStore(), CookiePolicy.ACCEPT_ALL); + } + + /** + * 自定义{@link CookieManager} + * + * @param customCookieManager 自定义的{@link CookieManager} + */ + public static void setCookieManager(CookieManager customCookieManager) { + cookieManager = customCookieManager; + } + + /** + * 获取全局{@link CookieManager} + * + * @return {@link CookieManager} + */ + public static CookieManager getCookieManager() { + return cookieManager; + } + + /** + * 将本地存储的Cookie信息附带到Http请求中,不覆盖用户定义好的Cookie + * + * @param conn {@link HttpConnection} + */ + public static void add(HttpConnection conn) { + if(null == cookieManager) { + // 全局Cookie管理器关闭 + return; + } + + Map> cookieHeader; + try { + cookieHeader = cookieManager.get(URLUtil.toURI(conn.getUrl()), new HashMap>(0)); + } catch (IOException e) { + throw new IORuntimeException(e); + } + + if(log.isDebugEnabled() && MapUtil.isNotEmpty(cookieHeader)) { + log.debug("Add Cookie from local store: {}", cookieHeader.get(Header.COOKIE.toString())); + } + + // 不覆盖模式回填Cookie头,这样用户定义的Cookie将优先 + conn.header(cookieHeader, false); + } + + /** + * 存储响应的Cookie信息到本地 + * + * @param conn {@link HttpConnection} + */ + public static void store(HttpConnection conn) { + if(null == cookieManager) { + // 全局Cookie管理器关闭 + return; + } + + if(log.isDebugEnabled()) { + String setCookie = conn.header(Header.SET_COOKIE); + if(StrUtil.isNotEmpty(setCookie)) { + log.debug("Store Cookie: {}", setCookie); + } + } + + try { + cookieManager.put(URLUtil.toURI(conn.getUrl()), conn.headers()); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } +} diff --git a/hutool-http/src/main/java/cn/hutool/http/cookie/ThreadLocalCookieStore.java b/hutool-http/src/main/java/cn/hutool/http/cookie/ThreadLocalCookieStore.java new file mode 100644 index 000000000..edaff4896 --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/cookie/ThreadLocalCookieStore.java @@ -0,0 +1,75 @@ +package cn.hutool.http.cookie; + +import java.net.CookieManager; +import java.net.CookieStore; +import java.net.HttpCookie; +import java.net.URI; +import java.util.List; + +/** + * 线程隔离的Cookie存储。多线程环境下Cookie隔离使用,防止Cookie覆盖
+ * + * 见:https://stackoverflow.com/questions/16305486/cookiemanager-for-multiple-threads + * + * @author looly + * @since 4.1.18 + */ +public class ThreadLocalCookieStore implements CookieStore { + + private final static ThreadLocal STORES = new ThreadLocal() { + @Override + protected synchronized CookieStore initialValue() { + /* InMemoryCookieStore */ + return (new CookieManager()).getCookieStore(); + } + }; + + /** + * 获取本线程下的CookieStore + * + * @return CookieStore + */ + public CookieStore getCookieStore() { + return STORES.get(); + } + + /** + * 移除当前线程的Cookie + * + * @return this + */ + public ThreadLocalCookieStore removeCurrent() { + STORES.remove(); + return this; + } + + @Override + public void add(URI uri, HttpCookie cookie) { + getCookieStore().add(uri, cookie); + } + + @Override + public List get(URI uri) { + return getCookieStore().get(uri); + } + + @Override + public List getCookies() { + return getCookieStore().getCookies(); + } + + @Override + public List getURIs() { + return getCookieStore().getURIs(); + } + + @Override + public boolean remove(URI uri, HttpCookie cookie) { + return getCookieStore().remove(uri, cookie); + } + + @Override + public boolean removeAll() { + return getCookieStore().removeAll(); + } +} diff --git a/hutool-http/src/main/java/cn/hutool/http/cookie/package-info.java b/hutool-http/src/main/java/cn/hutool/http/cookie/package-info.java new file mode 100644 index 000000000..c3980e9bc --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/cookie/package-info.java @@ -0,0 +1,7 @@ +/** + * 自定义Cookie + * + * @author looly + * + */ +package cn.hutool.http.cookie; \ No newline at end of file diff --git a/hutool-http/src/main/java/cn/hutool/http/package-info.java b/hutool-http/src/main/java/cn/hutool/http/package-info.java new file mode 100644 index 000000000..10fc74c38 --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/package-info.java @@ -0,0 +1,7 @@ +/** + * Hutool-http针对JDK的HttpUrlConnection做一层封装,简化了HTTPS请求、文件上传、Cookie记忆等操作,使Http请求变得无比简单。 + * + * @author looly + * + */ +package cn.hutool.http; \ No newline at end of file diff --git a/hutool-http/src/main/java/cn/hutool/http/ssl/AndroidSupportSSLFactory.java b/hutool-http/src/main/java/cn/hutool/http/ssl/AndroidSupportSSLFactory.java new file mode 100644 index 000000000..774c2d4da --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/ssl/AndroidSupportSSLFactory.java @@ -0,0 +1,28 @@ +package cn.hutool.http.ssl; + +import static cn.hutool.http.ssl.SSLSocketFactoryBuilder.SSLv3; +import static cn.hutool.http.ssl.SSLSocketFactoryBuilder.TLSv1; +import static cn.hutool.http.ssl.SSLSocketFactoryBuilder.TLSv11; +import static cn.hutool.http.ssl.SSLSocketFactoryBuilder.TLSv12; + +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; + +/** + * 兼容android低版本SSL连接 + * 咱在测试HttpUrlConnection的时候 + * 发现一部分手机无法连接[GithubPage] + * + * 最后发现原来是某些SSL协议没有开启 + * @author MikaGuraNTK + */ +public class AndroidSupportSSLFactory extends CustomProtocolsSSLFactory { + + // Android低版本不重置的话某些SSL访问就会失败 + private static String[] protocols = {SSLv3, TLSv1, TLSv11, TLSv12}; + + public AndroidSupportSSLFactory() throws KeyManagementException, NoSuchAlgorithmException { + super(protocols); + } + +} \ No newline at end of file diff --git a/hutool-http/src/main/java/cn/hutool/http/ssl/CustomProtocolsSSLFactory.java b/hutool-http/src/main/java/cn/hutool/http/ssl/CustomProtocolsSSLFactory.java new file mode 100644 index 000000000..1c7c521f0 --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/ssl/CustomProtocolsSSLFactory.java @@ -0,0 +1,100 @@ +package cn.hutool.http.ssl; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; + +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +/** + * 自定义支持协议类型的SSLSocketFactory + * + * @author looly + * + */ +public class CustomProtocolsSSLFactory extends SSLSocketFactory { + + private String[] protocols; + private SSLSocketFactory base; + + /** + * 构造 + * + * @param protocols 支持协议列表 + * @throws KeyManagementException KeyManagementException + * @throws NoSuchAlgorithmException NoSuchAlgorithmException + */ + public CustomProtocolsSSLFactory(String... protocols) throws KeyManagementException, NoSuchAlgorithmException { + super(); + this.protocols = protocols; + this.base = SSLSocketFactoryBuilder.create().build(); + } + + @Override + public String[] getDefaultCipherSuites() { + return base.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return base.getSupportedCipherSuites(); + } + + @Override + public Socket createSocket() throws IOException { + SSLSocket sslSocket = (SSLSocket) base.createSocket(); + resetProtocols(sslSocket); + return sslSocket; + } + + @Override + public SSLSocket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { + SSLSocket socket = (SSLSocket) base.createSocket(s, host, port, autoClose); + resetProtocols(socket); + return socket; + } + + @Override + public Socket createSocket(String host, int port) throws IOException { + SSLSocket socket = (SSLSocket) base.createSocket(host, port); + resetProtocols(socket); + return socket; + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException { + + SSLSocket socket = (SSLSocket) base.createSocket(host, port, localHost, localPort); + resetProtocols(socket); + return socket; + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + + SSLSocket socket = (SSLSocket) base.createSocket(host, port); + resetProtocols(socket); + return socket; + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { + + SSLSocket socket = (SSLSocket) base.createSocket(address, port, localAddress, localPort); + resetProtocols(socket); + return socket; + } + + /** + * 重置可用策略 + * + * @param socket SSLSocket + */ + private void resetProtocols(SSLSocket socket) { + socket.setEnabledProtocols(protocols); + } + +} \ No newline at end of file diff --git a/hutool-http/src/main/java/cn/hutool/http/ssl/DefaultTrustManager.java b/hutool-http/src/main/java/cn/hutool/http/ssl/DefaultTrustManager.java new file mode 100644 index 000000000..5159718c5 --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/ssl/DefaultTrustManager.java @@ -0,0 +1,27 @@ +package cn.hutool.http.ssl; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import javax.net.ssl.X509TrustManager; + +/** + * 证书管理 + * @author Looly + * + */ +public class DefaultTrustManager implements X509TrustManager { + + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } +} diff --git a/hutool-http/src/main/java/cn/hutool/http/ssl/SSLSocketFactoryBuilder.java b/hutool-http/src/main/java/cn/hutool/http/ssl/SSLSocketFactoryBuilder.java new file mode 100644 index 000000000..1d79f4328 --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/ssl/SSLSocketFactoryBuilder.java @@ -0,0 +1,113 @@ +package cn.hutool.http.ssl; + +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; + +/** + * SSLSocketFactory构建器 + * @author Looly + * + */ +public class SSLSocketFactoryBuilder{ + + /** Supports some version of SSL; may support other versions */ + public static final String SSL = "SSL"; + /** Supports SSL version 2 or later; may support other versions */ + public static final String SSLv2 = "SSLv2"; + /** Supports SSL version 3; may support other versions */ + public static final String SSLv3 = "SSLv3"; + + /** Supports some version of TLS; may support other versions */ + public static final String TLS = "TLS"; + /** Supports RFC 2246: TLS version 1.0 ; may support other versions */ + public static final String TLSv1 = "TLSv1"; + /** Supports RFC 4346: TLS version 1.1 ; may support other versions */ + public static final String TLSv11 = "TLSv1.1"; + /** Supports RFC 5246: TLS version 1.2 ; may support other versions */ + public static final String TLSv12 = "TLSv1.2"; + + private String protocol = TLS; + private KeyManager[] keyManagers; + private TrustManager[] trustManagers = {new DefaultTrustManager()}; + private SecureRandom secureRandom = new SecureRandom(); + + + /** + * 创建 SSLSocketFactoryBuilder + * @return SSLSocketFactoryBuilder + */ + public static SSLSocketFactoryBuilder create(){ + return new SSLSocketFactoryBuilder(); + } + + /** + * 设置协议 + * @param protocol 协议 + * @return 自身 + */ + public SSLSocketFactoryBuilder setProtocol(String protocol){ + if(StrUtil.isNotBlank(protocol)){ + this.protocol = protocol; + } + return this; + } + + /** + * 设置信任信息 + * + * @param trustManagers TrustManager列表 + * @return 自身 + */ + public SSLSocketFactoryBuilder setTrustManagers(TrustManager... trustManagers) { + if (ArrayUtil.isNotEmpty(trustManagers)) { + this.trustManagers = trustManagers; + } + return this; + } + + /** + * 设置 JSSE key managers + * + * @param keyManagers JSSE key managers + * @return 自身 + */ + public SSLSocketFactoryBuilder setKeyManagers(KeyManager... keyManagers) { + if (ArrayUtil.isNotEmpty(keyManagers)) { + this.keyManagers = keyManagers; + } + return this; + } + + /** + * 设置 SecureRandom + * @param secureRandom SecureRandom + * @return 自己 + */ + public SSLSocketFactoryBuilder setSecureRandom(SecureRandom secureRandom){ + if(null != secureRandom){ + this.secureRandom = secureRandom; + } + return this; + } + + /** + * 构建SSLSocketFactory + * @return SSLSocketFactory + * @throws NoSuchAlgorithmException 无此算法 + * @throws KeyManagementException Key管理异常 + */ + public SSLSocketFactory build() throws NoSuchAlgorithmException, KeyManagementException{ + SSLContext sslContext = SSLContext.getInstance(protocol); + sslContext.init(this.keyManagers, this.trustManagers, this.secureRandom); + return sslContext.getSocketFactory(); + } +} diff --git a/hutool-http/src/main/java/cn/hutool/http/ssl/TrustAnyHostnameVerifier.java b/hutool-http/src/main/java/cn/hutool/http/ssl/TrustAnyHostnameVerifier.java new file mode 100644 index 000000000..dfa0217d3 --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/ssl/TrustAnyHostnameVerifier.java @@ -0,0 +1,17 @@ +package cn.hutool.http.ssl; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSession; + +/** + * https 域名校验 + * + * @author Looly + */ +public class TrustAnyHostnameVerifier implements HostnameVerifier { + + @Override + public boolean verify(String hostname, SSLSession session) { + return true;// 直接返回true + } +} diff --git a/hutool-http/src/main/java/cn/hutool/http/ssl/package-info.java b/hutool-http/src/main/java/cn/hutool/http/ssl/package-info.java new file mode 100644 index 000000000..218275077 --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/ssl/package-info.java @@ -0,0 +1,7 @@ +/** + * SSL封装 + * + * @author looly + * + */ +package cn.hutool.http.ssl; \ No newline at end of file diff --git a/hutool-http/src/main/java/cn/hutool/http/useragent/Browser.java b/hutool-http/src/main/java/cn/hutool/http/useragent/Browser.java new file mode 100644 index 000000000..2e7a5c390 --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/useragent/Browser.java @@ -0,0 +1,87 @@ +package cn.hutool.http.useragent; + +import java.util.List; +import java.util.regex.Pattern; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ReUtil; + +/** + * 浏览器对象 + * + * @author looly + * @since 4.2.1 + */ +public class Browser extends UserAgentInfo { + + /** 未知 */ + public static final Browser Unknown = new Browser(NameUnknown, null, null); + /** 其它版本 */ + public static final String Other_Version = "[\\/ ]([\\d\\w\\.\\-]+)"; + + /** + * 支持的浏览器类型 + */ + public static final List browers = CollUtil.newArrayList(// + new Browser("MSEdge", "Edge", "edge\\/([\\d\\w\\.\\-]+)"), // + new Browser("Chrome", "chrome", "chrome\\/([\\d\\w\\.\\-]+)"), // + new Browser("Firefox", "firefox", Other_Version), // + new Browser("IEMobile", "iemobile", Other_Version), // + new Browser("Safari", "safari", "version\\/([\\d\\w\\.\\-]+)"), // + new Browser("Opera", "opera", Other_Version), // + new Browser("Konqueror", "konqueror", Other_Version), // + new Browser("PS3", "playstation 3", "([\\d\\w\\.\\-]+)\\)\\s*$"), // + new Browser("PSP", "playstation portable", "([\\d\\w\\.\\-]+)\\)?\\s*$"), // + new Browser("Lotus", "lotus.notes", "Lotus-Notes\\/([\\w.]+)"), // + new Browser("Thunderbird", "thunderbird", Other_Version), // + new Browser("Netscape", "netscape", Other_Version), // + new Browser("Seamonkey", "seamonkey", Other_Version), // + new Browser("Outlook", "microsoft.outlook", Other_Version), // + new Browser("Evolution", "evolution", Other_Version), // + new Browser("MSIE", "msie", "msie ([\\d\\w\\.\\-]+)"), // + new Browser("MSIE11", "rv:11", "rv:([\\d\\w\\.\\-]+)"), // + new Browser("Gabble", "Gabble", "Gabble\\/([\\d\\w\\.\\-]+)"), // + new Browser("Yammer Desktop", "AdobeAir", "([\\d\\w\\.\\-]+)\\/Yammer"), // + new Browser("Yammer Mobile", "Yammer[\\s]+([\\d\\w\\.\\-]+)", "Yammer[\\s]+([\\d\\w\\.\\-]+)"), // + new Browser("Apache HTTP Client", "Apache\\\\-HttpClient", "Apache\\-HttpClient\\/([\\d\\w\\.\\-]+)"), // + new Browser("BlackBerry", "BlackBerry", "BlackBerry[\\d]+\\/([\\d\\w\\.\\-]+)")// + ); + + private Pattern versionPattern; + + + /** + * 构造 + * + * @param name 浏览器名称 + * @param regex 关键字或表达式 + */ + public Browser(String name, String regex, String versionRegex) { + super(name, regex); + if (Other_Version.equals(versionRegex)) { + versionRegex = name + versionRegex; + } + if (null != versionRegex) { + this.versionPattern = Pattern.compile(versionRegex, Pattern.CASE_INSENSITIVE); + } + } + + /** + * 获取浏览器版本 + * + * @param userAgentString User-Agent字符串 + * @return 版本 + */ + public String getVersion(String userAgentString) { + return ReUtil.getGroup1(this.versionPattern, userAgentString); + } + + /** + * 是否移动浏览器 + * + * @return 是否移动浏览器 + */ + public boolean isMobile() { + return "PSP".equals(this.getName()); + } +} diff --git a/hutool-http/src/main/java/cn/hutool/http/useragent/Engine.java b/hutool-http/src/main/java/cn/hutool/http/useragent/Engine.java new file mode 100644 index 000000000..4e9a9664b --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/useragent/Engine.java @@ -0,0 +1,43 @@ +package cn.hutool.http.useragent; + +import java.util.List; + +import cn.hutool.core.collection.CollUtil; + +/** + * 引擎对象 + * + * @author looly + * @since 4.2.1 + */ +public class Engine extends UserAgentInfo { + + /** 未知 */ + public static final Engine Unknown = new Engine(NameUnknown, null); + + /** + * 支持的引擎类型 + */ + public static final List engines = CollUtil.newArrayList(// + new Engine("Trident", "trident"), // + new Engine("Webkit", "webkit"), // + new Engine("Chrome", "chrome"), // + new Engine("Opera", "opera"), // + new Engine("Presto", "presto"), // + new Engine("Gecko", "gecko"), // + new Engine("KHTML", "khtml"), // + new Engine("Konqeror", "konqueror"), // + new Engine("MIDP", "MIDP")// + ); + + /** + * 构造 + * + * @param name 引擎名称 + * @param regex 关键字或表达式 + */ + public Engine(String name, String regex) { + super(name, regex); + } + +} diff --git a/hutool-http/src/main/java/cn/hutool/http/useragent/OS.java b/hutool-http/src/main/java/cn/hutool/http/useragent/OS.java new file mode 100644 index 000000000..d3b4fce83 --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/useragent/OS.java @@ -0,0 +1,59 @@ +package cn.hutool.http.useragent; + +import java.util.List; + +import cn.hutool.core.collection.CollUtil; + +/** + * 系统对象 + * + * @author looly + * @since 4.2.1 + */ +public class OS extends UserAgentInfo { + + /** 未知 */ + public static final OS Unknown = new OS(NameUnknown, null); + + /** + * 支持的引擎类型 + */ + public static final List oses = CollUtil.newArrayList(// + new OS("Windows 10 or Windows Server 2016","windows nt 10\\.0"),// + new OS("Windows 8.1 or Winsows Server 2012R2","windows nt 6\\.3"),// + new OS("Windows 8 or Winsows Server 2012","windows nt 6\\.2"),// + new OS("Windows Vista", "windows nt 6\\.0"), // + new OS("Windows 7 or Windows Server 2008R2", "windows nt 6\\.1"), // + new OS("Windows 2003", "windows nt 5\\.2"), // + new OS("Windows XP", "windows nt 5\\.1"), // + new OS("Windows 2000", "windows nt 5\\.0"), // + new OS("Windows Phone", "windows (ce|phone|mobile)( os)?"), // + new OS("Windows", "windows"), // + new OS("OSX", "os x (\\d+)[._](\\d+)"), // + new OS("Android","Android"),// + new OS("Linux", "linux"), // + new OS("Wii", "wii"), // + new OS("PS3", "playstation 3"), // + new OS("PSP", "playstation portable"), // + new OS("iPad", "\\(iPad.*os (\\d+)[._](\\d+)"), // + new OS("iPhone", "\\(iPhone.*os (\\d+)[._](\\d+)"), // + new OS("YPod", "iPod touch[\\s\\;]+iPhone.*os (\\d+)[._](\\d+)"), // + new OS("YPad", "iPad[\\s\\;]+iPhone.*os (\\d+)[._](\\d+)"), // + new OS("YPhone", "iPhone[\\s\\;]+iPhone.*os (\\d+)[._](\\d+)"), // + new OS("Symbian", "symbian(os)?"), // + new OS("Darwin", "Darwin\\/([\\d\\w\\.\\-]+)"), // + new OS("Adobe Air", "AdobeAir\\/([\\d\\w\\.\\-]+)"), // + new OS("Java", "Java[\\s]+([\\d\\w\\.\\-]+)")// + ); + + /** + * 构造 + * + * @param name 系统名称 + * @param regex 关键字或表达式 + */ + public OS(String name, String regex) { + super(name, regex); + } + +} diff --git a/hutool-http/src/main/java/cn/hutool/http/useragent/Platform.java b/hutool-http/src/main/java/cn/hutool/http/useragent/Platform.java new file mode 100644 index 000000000..e9f1ff232 --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/useragent/Platform.java @@ -0,0 +1,71 @@ +package cn.hutool.http.useragent; + +import java.util.ArrayList; +import java.util.List; + +import cn.hutool.core.collection.CollUtil; + +/** + * 平台对象 + * + * @author looly + * @since 4.2.1 + */ +public class Platform extends UserAgentInfo { + + /** 未知 */ + public static final Platform Unknown = new Platform(NameUnknown, null); + + /** + * 支持的移动平台类型 + */ + public static final List mobilePlatforms = CollUtil.newArrayList(// + new Platform("Windows Phone", "windows (ce|phone|mobile)( os)?"), // + new Platform("iPad", "ipad"), // + new Platform("iPod", "ipod"), // + new Platform("iPhone", "iphone"), // + new Platform("Android", "android"), // + new Platform("Symbian", "symbian(os)?"), // + new Platform("Blackberry", "blackberry") // + ); + + /** + * 支持的桌面平台类型 + */ + public static final List desktopPlatforms=CollUtil.newArrayList(// + new Platform("Windows", "windows"), // + new Platform("Mac", "(macintosh|darwin)"), // + new Platform("Linux", "linux"), // + new Platform("Wii", "wii"), // + new Platform("Playstation", "playstation"), // + new Platform("Java", "java") // + ); + + /** + * 支持的平台类型 + */ + public static final List platforms; + static { + platforms=new ArrayList(13); + platforms.addAll(mobilePlatforms); + platforms.addAll(desktopPlatforms); + } + + /** + * 构造 + * + * @param name 平台名称 + * @param regex 关键字或表达式 + */ + public Platform(String name, String regex) { + super(name, regex); + } + + /** + * 是否为移动平台 + * @return 是否为移动平台 + */ + public boolean isMobile() { + return mobilePlatforms.contains(this); + } +} diff --git a/hutool-http/src/main/java/cn/hutool/http/useragent/UserAgent.java b/hutool-http/src/main/java/cn/hutool/http/useragent/UserAgent.java new file mode 100644 index 000000000..a9a352811 --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/useragent/UserAgent.java @@ -0,0 +1,152 @@ +package cn.hutool.http.useragent; + +/** + * User-Agent信息对象 + * + * @author looly + * @since 4.2.1 + */ +public class UserAgent { + + /** 是否为移动平台 */ + private boolean mobile; + /** 浏览器类型 */ + private Browser browser; + /** 平台类型 */ + private Platform platform; + /** 系统类型 */ + private OS os; + /** 引擎类型 */ + private Engine engine; + /** 浏览器版本 */ + private String version; + /** 引擎版本 */ + private String engineVersion; + + /** + * 是否为移动平台 + * + * @return 是否为移动平台 + */ + public boolean isMobile() { + return mobile; + } + + /** + * 设置是否为移动平台 + * + * @param mobile 是否为移动平台 + */ + public void setMobile(boolean mobile) { + this.mobile = mobile; + } + + /** + * 获取浏览器类型 + * + * @return 浏览器类型 + */ + public Browser getBrowser() { + return browser; + } + + /** + * 设置浏览器类型 + * + * @param browser 浏览器类型 + */ + public void setBrowser(Browser browser) { + this.browser = browser; + } + + /** + * 获取平台类型 + * + * @return 平台类型 + */ + public Platform getPlatform() { + return platform; + } + + /** + * 设置平台类型 + * + * @param platform 平台类型 + */ + public void setPlatform(Platform platform) { + this.platform = platform; + } + + /** + * 获取系统类型 + * + * @return 系统类型 + */ + public OS getOs() { + return os; + } + + /** + * 设置系统类型 + * + * @param os 系统类型 + */ + public void setOs(OS os) { + this.os = os; + } + + /** + * 获取引擎类型 + * + * @return 引擎类型 + */ + public Engine getEngine() { + return engine; + } + + /** + * 设置引擎类型 + * + * @param engine 引擎类型 + */ + public void setEngine(Engine engine) { + this.engine = engine; + } + + /** + * 获取浏览器版本 + * + * @return 浏览器版本 + */ + public String getVersion() { + return version; + } + + /** + * 设置浏览器版本 + * + * @param version 浏览器版本 + */ + public void setVersion(String version) { + this.version = version; + } + + /** + * 获取引擎版本 + * + * @return 引擎版本 + */ + public String getEngineVersion() { + return engineVersion; + } + + /** + * 设置引擎版本 + * + * @param engineVersion 引擎版本 + */ + public void setEngineVersion(String engineVersion) { + this.engineVersion = engineVersion; + } + +} diff --git a/hutool-http/src/main/java/cn/hutool/http/useragent/UserAgentInfo.java b/hutool-http/src/main/java/cn/hutool/http/useragent/UserAgentInfo.java new file mode 100644 index 000000000..1d2a132b6 --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/useragent/UserAgentInfo.java @@ -0,0 +1,114 @@ +package cn.hutool.http.useragent; + +import java.util.regex.Pattern; + +import cn.hutool.core.util.ReUtil; + +/** + * User-agent信息 + * + * @author looly + * @since 4.2.1 + */ +public class UserAgentInfo { + + public static final String NameUnknown = "Unknown"; + + /** 信息名称 */ + private String name; + /** 信息匹配模式 */ + private Pattern pattern; + + /** + * 构造 + * + * @param name 名字 + * @param regex 表达式 + */ + public UserAgentInfo(String name, String regex) { + this(name, (null == regex) ? null : Pattern.compile(regex, Pattern.CASE_INSENSITIVE)); + } + + /** + * 构造 + * + * @param name 名字 + * @param pattern 匹配模式 + */ + public UserAgentInfo(String name, Pattern pattern) { + this.name = name; + this.pattern = pattern; + } + + /** + * 获取信息名称 + * + * @return 信息名称 + */ + public String getName() { + return name; + } + + /** + * 获取匹配模式 + * + * @return 匹配模式 + */ + public Pattern getPattern() { + return pattern; + } + + /** + * 指定内容中是否包含匹配此信息的内容 + * + * @param content User-Agent字符串 + * @return 是否包含匹配此信息的内容 + */ + public boolean isMatch(String content) { + return ReUtil.contains(this.pattern, content); + } + + /** + * 是否为Unknown + * + * @return 是否为Unknown + */ + public boolean isUnknown() { + return NameUnknown.equals(this.name); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((name == null) ? 0 : name.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 UserAgentInfo other = (UserAgentInfo) obj; + if (name == null) { + if (other.name != null) { + return false; + } + } else if (!name.equals(other.name)) { + return false; + } + return true; + } + + @Override + public String toString() { + return this.name; + } +} diff --git a/hutool-http/src/main/java/cn/hutool/http/useragent/UserAgentParser.java b/hutool-http/src/main/java/cn/hutool/http/useragent/UserAgentParser.java new file mode 100644 index 000000000..579d8d674 --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/useragent/UserAgentParser.java @@ -0,0 +1,114 @@ +package cn.hutool.http.useragent; + +import java.util.regex.Pattern; + +import cn.hutool.core.util.ReUtil; + +/** + * User-Agent解析器 + * + * @author looly + * @since 4.2.1 + */ +public class UserAgentParser { + + /** + * 解析User-Agent + * + * @param userAgentString User-Agent字符串 + * @return {@link UserAgent} + */ + public static UserAgent parse(String userAgentString) { + final UserAgent userAgent = new UserAgent(); + + final Browser browser = parseBrowser(userAgentString); + userAgent.setBrowser(parseBrowser(userAgentString)); + userAgent.setVersion(browser.getVersion(userAgentString)); + + final Engine engine = parseEngine(userAgentString); + userAgent.setEngine(engine); + if (false == engine.isUnknown()) { + userAgent.setEngineVersion(parseEngineVersion(engine, userAgentString)); + } + userAgent.setOs(parseOS(userAgentString)); + final Platform platform = parsePlatform(userAgentString); + userAgent.setPlatform(platform); + userAgent.setMobile(platform.isMobile() || browser.isMobile()); + + + return userAgent; + } + + /** + * 解析浏览器类型 + * + * @param userAgentString User-Agent字符串 + * @return 浏览器类型 + */ + private static Browser parseBrowser(String userAgentString) { + for (Browser brower : Browser.browers) { + if (brower.isMatch(userAgentString)) { + return brower; + } + } + return Browser.Unknown; + } + + /** + * 解析引擎类型 + * + * @param userAgentString User-Agent字符串 + * @return 引擎类型 + */ + private static Engine parseEngine(String userAgentString) { + for (Engine engine : Engine.engines) { + if (engine.isMatch(userAgentString)) { + return engine; + } + } + return Engine.Unknown; + } + + /** + * 解析引擎版本 + * + * @param engine 引擎 + * @param userAgentString User-Agent字符串 + * @return 引擎版本 + */ + private static String parseEngineVersion(Engine engine, String userAgentString) { + final String regexp = engine.getName() + "[\\/\\- ]([\\d\\w\\.\\-]+)"; + final Pattern pattern = Pattern.compile(regexp, Pattern.CASE_INSENSITIVE); + return ReUtil.getGroup1(pattern, userAgentString); + } + + /** + * 解析系统类型 + * + * @param userAgentString User-Agent字符串 + * @return 系统类型 + */ + private static OS parseOS(String userAgentString) { + for (OS os : OS.oses) { + if (os.isMatch(userAgentString)) { + return os; + } + } + return OS.Unknown; + } + + /** + * 解析平台类型 + * + * @param userAgentString User-Agent字符串 + * @return 平台类型 + */ + private static Platform parsePlatform(String userAgentString) { + for (Platform platform : Platform.platforms) { + if (platform.isMatch(userAgentString)) { + return platform; + } + } + return Platform.Unknown; + } +} diff --git a/hutool-http/src/main/java/cn/hutool/http/useragent/UserAgentUtil.java b/hutool-http/src/main/java/cn/hutool/http/useragent/UserAgentUtil.java new file mode 100644 index 000000000..d048204d4 --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/useragent/UserAgentUtil.java @@ -0,0 +1,20 @@ +package cn.hutool.http.useragent; + +/** + * User-Agent工具类 + * + * @author looly + * + */ +public class UserAgentUtil { + + /** + * 解析User-Agent + * + * @param userAgentString User-Agent字符串 + * @return {@link UserAgent} + */ + public static UserAgent parse(String userAgentString) { + return UserAgentParser.parse(userAgentString); + } +} diff --git a/hutool-http/src/main/java/cn/hutool/http/useragent/package-info.java b/hutool-http/src/main/java/cn/hutool/http/useragent/package-info.java new file mode 100644 index 000000000..84339f543 --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/useragent/package-info.java @@ -0,0 +1,7 @@ +/** + * User-Agent解析 + * + * @author looly + * + */ +package cn.hutool.http.useragent; \ No newline at end of file diff --git a/hutool-http/src/main/java/cn/hutool/http/webservice/SoapClient.java b/hutool-http/src/main/java/cn/hutool/http/webservice/SoapClient.java new file mode 100644 index 000000000..30225257d --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/webservice/SoapClient.java @@ -0,0 +1,555 @@ +package cn.hutool.http.webservice; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import javax.xml.XMLConstants; +import javax.xml.namespace.QName; +import javax.xml.soap.MessageFactory; +import javax.xml.soap.MimeHeaders; +import javax.xml.soap.Name; +import javax.xml.soap.SOAPBodyElement; +import javax.xml.soap.SOAPElement; +import javax.xml.soap.SOAPException; +import javax.xml.soap.SOAPHeader; +import javax.xml.soap.SOAPHeaderElement; +import javax.xml.soap.SOAPMessage; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.XmlUtil; +import cn.hutool.http.HttpGlobalConfig; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; + +/** + * SOAP客户端 + * + * @author looly + * @since 4.5.4 + */ +public class SoapClient { + + /** XML消息体的Content-Type */ + private static final String TEXT_XML_CONTENT_TYPE = "text/xml;charset="; + + /** 请求的URL地址 */ + private String url; + /** 编码 */ + private Charset charset = CharsetUtil.CHARSET_UTF_8; + /** SOAP消息 */ + private SOAPMessage message; + /** 消息方法节点 */ + private SOAPBodyElement methodEle; + /** 应用于方法上的命名空间URI */ + private String namespaceURI; + /** 消息工厂,用于创建消息 */ + private MessageFactory factory; + /** 默认连接超时 */ + private int connectionTimeout = HttpGlobalConfig.getTimeout(); + /** 默认读取超时 */ + private int readTimeout = HttpGlobalConfig.getTimeout(); + + /** + * 创建SOAP客户端,默认使用soap1.1版本协议 + * + * @param url WS的URL地址 + * @return {@link SoapClient} + */ + public static SoapClient create(String url) { + return new SoapClient(url); + } + + /** + * 创建SOAP客户端 + * + * @param url WS的URL地址 + * @param protocol 协议,见{@link SoapProtocol} + * @return {@link SoapClient} + */ + public static SoapClient create(String url, SoapProtocol protocol) { + return new SoapClient(url, protocol); + } + + /** + * 创建SOAP客户端 + * + * @param url WS的URL地址 + * @param protocol 协议,见{@link SoapProtocol} + * @param namespaceURI 方法上的命名空间URI + * @return {@link SoapClient} + * @since 4.5.6 + */ + public static SoapClient create(String url, SoapProtocol protocol, String namespaceURI) { + return new SoapClient(url, protocol, namespaceURI); + } + + /** + * 构造,默认使用soap1.1版本协议 + * + * @param url WS的URL地址 + */ + public SoapClient(String url) { + this(url, SoapProtocol.SOAP_1_1); + } + + /** + * 构造 + * + * @param url WS的URL地址 + * @param protocol 协议版本,见{@link SoapProtocol} + */ + public SoapClient(String url, SoapProtocol protocol) { + this(url, protocol, null); + } + + /** + * 构造 + * + * @param url WS的URL地址 + * @param protocol 协议版本,见{@link SoapProtocol} + * @param namespaceURI 方法上的命名空间URI + * @since 4.5.6 + */ + public SoapClient(String url, SoapProtocol protocol, String namespaceURI) { + this.url = url; + this.namespaceURI = namespaceURI; + init(protocol); + } + + /** + * 初始化 + * + * @param protocol 协议版本枚举,见{@link SoapProtocol} + * @return this + */ + public SoapClient init(SoapProtocol protocol) { + // 创建消息工厂 + try { + this.factory = MessageFactory.newInstance(protocol.getValue()); + // 根据消息工厂创建SoapMessage + this.message = factory.createMessage(); + } catch (SOAPException e) { + throw new SoapRuntimeException(e); + } + + return this; + } + + /** + * 设置编码 + * + * @param charset 编码 + * @return this + */ + public SoapClient setCharset(Charset charset) { + this.charset = charset; + try { + this.message.setProperty(SOAPMessage.CHARACTER_SET_ENCODING, this.charset.toString()); + this.message.setProperty(SOAPMessage.WRITE_XML_DECLARATION, "true"); + } catch (SOAPException e) { + // ignore + } + + return this; + } + + /** + * 设置Webservice请求地址 + * + * @param url Webservice请求地址 + * @return this + */ + public SoapClient setUrl(String url) { + this.url = url; + return this; + } + + /** + * 设置头信息 + * + * @param name 头信息标签名 + * @return this + */ + public SoapClient setHeader(QName name) { + return setHeader(name, null, null, null, null); + } + + /** + * 设置头信息 + * + * @param name 头信息标签名 + * @param actorURI 中间的消息接收者 + * @param roleUri Role的URI + * @param mustUnderstand 标题项对于要对其进行处理的接收者来说是强制的还是可选的 + * @param relay relay属性 + * @return this + */ + public SoapClient setHeader(QName name, String actorURI, String roleUri, Boolean mustUnderstand, Boolean relay) { + SOAPHeader header; + SOAPHeaderElement ele; + try { + header = this.message.getSOAPHeader(); + ele = header.addHeaderElement(name); + if (StrUtil.isNotBlank(roleUri)) { + ele.setRole(roleUri); + } + if (null != relay) { + ele.setRelay(relay); + } + } catch (SOAPException e) { + throw new SoapRuntimeException(e); + } + + if (StrUtil.isNotBlank(actorURI)) { + ele.setActor(actorURI); + } + if (null != mustUnderstand) { + ele.setMustUnderstand(mustUnderstand); + } + + return this; + } + + /** + * 设置请求方法 + * + * @param name 方法名及其命名空间 + * @param params 参数 + * @param useMethodPrefix 是否使用方法的命名空间前缀 + * @return this + */ + public SoapClient setMethod(Name name, Map params, boolean useMethodPrefix) { + return setMethod(new QName(name.getURI(), name.getLocalName(), name.getPrefix()), params, useMethodPrefix); + } + + /** + * 设置请求方法 + * + * @param name 方法名及其命名空间 + * @param params 参数 + * @param useMethodPrefix 是否使用方法的命名空间前缀 + * @return this + */ + public SoapClient setMethod(QName name, Map params, boolean useMethodPrefix) { + setMethod(name); + final String prefix = name.getPrefix(); + final SOAPBodyElement methodEle = this.methodEle; + for (Entry entry : MapUtil.wrap(params)) { + setParam(methodEle, entry.getKey(), entry.getValue(), prefix); + } + + return this; + } + + /** + * 设置请求方法
+ * 方法名自动识别前缀,前缀和方法名使用“:”分隔
+ * 当识别到前缀后,自动添加xmlns属性,关联到默认的namespaceURI + * + * @param methodName 方法名 + * @return this + */ + public SoapClient setMethod(String methodName) { + return setMethod(methodName, ObjectUtil.defaultIfNull(this.namespaceURI, XMLConstants.NULL_NS_URI)); + } + + /** + * 设置请求方法
+ * 方法名自动识别前缀,前缀和方法名使用“:”分隔
+ * 当识别到前缀后,自动添加xmlns属性,关联到传入的namespaceURI + * + * @param methodName 方法名(可有前缀也可无) + * @param namespaceURI 命名空间URI + * @return this + */ + public SoapClient setMethod(String methodName, String namespaceURI) { + final List methodNameList = StrUtil.split(methodName, ':'); + final QName qName; + if (2 == methodNameList.size()) { + qName = new QName(namespaceURI, methodNameList.get(1), methodNameList.get(0)); + } else { + qName = new QName(namespaceURI, methodName); + } + return setMethod(qName); + } + + /** + * 设置请求方法 + * + * @param name 方法名及其命名空间 + * @return this + */ + public SoapClient setMethod(QName name) { + try { + this.methodEle = this.message.getSOAPBody().addBodyElement(name); + } catch (SOAPException e) { + throw new SoapRuntimeException(e); + } + + return this; + } + + /** + * 设置方法参数,使用方法的前缀 + * + * @param name 参数名 + * @param value 参数值,可以是字符串或Map或{@link SOAPElement} + * @return this + */ + public SoapClient setParam(String name, Object value) { + return setParam(name, value, true); + } + + /** + * 设置方法参数 + * + * @param name 参数名 + * @param value 参数值,可以是字符串或Map或{@link SOAPElement} + * @param useMethodPrefix 是否使用方法的命名空间前缀 + * @return this + */ + public SoapClient setParam(String name, Object value, boolean useMethodPrefix) { + setParam(this.methodEle, name, value, useMethodPrefix ? this.methodEle.getPrefix() : null); + return this; + } + + /** + * 批量设置参数,使用方法的前缀 + * + * @param params 参数列表 + * @return this + * @since 4.5.6 + */ + public SoapClient setParams(Map params) { + return setParams(params, true); + } + + /** + * 批量设置参数 + * + * @param params 参数列表 + * @param useMethodPrefix 是否使用方法的命名空间前缀 + * @return this + * @since 4.5.6 + */ + public SoapClient setParams(Map params, boolean useMethodPrefix) { + for (Entry entry : MapUtil.wrap(params)) { + setParam(entry.getKey(), entry.getValue(), useMethodPrefix); + } + return this; + } + + /** + * 获取方法节点
+ * 用于创建子节点等操作 + * + * @return {@link SOAPBodyElement} + * @since 4.5.6 + */ + public SOAPBodyElement getMethodEle() { + return this.methodEle; + } + + /** + * 获取SOAP消息对象 {@link SOAPMessage} + * + * @return {@link SOAPMessage} + * @since 4.5.6 + */ + public SOAPMessage getMessage() { + return this.message; + } + + /** + * 获取SOAP请求消息 + * + * @param pretty 是否格式化 + * @return 消息字符串 + */ + public String getMsgStr(boolean pretty) { + return SoapUtil.toString(this.message, pretty, this.charset); + } + + /** + * 将SOAP消息的XML内容输出到流 + * + * @param out 输出流 + * @return this + * @since 4.5.6 + */ + public SoapClient write(OutputStream out) { + try { + this.message.writeTo(out); + } catch (SOAPException | IOException e) { + throw new SoapRuntimeException(e); + } + return this; + } + + /** + * 设置超时,单位:毫秒
+ * 超时包括: + * + *
+	 * 1. 连接超时
+	 * 2. 读取响应超时
+	 * 
+ * + * @param milliseconds 超时毫秒数 + * @return this + * @see #setConnectionTimeout(int) + * @see #setReadTimeout(int) + */ + public SoapClient timeout(int milliseconds) { + setConnectionTimeout(milliseconds); + setReadTimeout(milliseconds); + return this; + } + + /** + * 设置连接超时,单位:毫秒 + * + * @param milliseconds 超时毫秒数 + * @return this + * @since 4.5.6 + */ + public SoapClient setConnectionTimeout(int milliseconds) { + this.connectionTimeout = milliseconds; + return this; + } + + /** + * 设置连接超时,单位:毫秒 + * + * @param milliseconds 超时毫秒数 + * @return this + * @since 4.5.6 + */ + public SoapClient setReadTimeout(int milliseconds) { + this.readTimeout = milliseconds; + return this; + } + + /** + * 执行Webservice请求,既发送SOAP内容 + * + * @return 返回结果 + */ + public SOAPMessage sendForMessage() { + final HttpResponse res = sendForResponse(); + final MimeHeaders headers = new MimeHeaders(); + for (Entry> entry : res.headers().entrySet()) { + if(StrUtil.isNotEmpty(entry.getKey())) { + headers.setHeader(entry.getKey(), CollUtil.get(entry.getValue(), 0)); + } + } + try { + return this.factory.createMessage(headers, res.bodyStream()); + } catch (IOException | SOAPException e) { + throw new SoapRuntimeException(e); + } finally { + IoUtil.close(res); + } + } + + /** + * 执行Webservice请求,既发送SOAP内容 + * + * @return 返回结果 + */ + public String send() { + return send(false); + } + + /** + * 执行Webservice请求,既发送SOAP内容 + * + * @param pretty 是否格式化 + * @return 返回结果 + */ + public String send(boolean pretty) { + final String body = sendForResponse().body(); + return pretty ? XmlUtil.format(body) : body; + } + + // -------------------------------------------------------------------------------------------------------- Private method start + /** + * 发送请求,获取异步响应 + * + * @return 响应对象 + */ + private HttpResponse sendForResponse() { + return HttpRequest.post(this.url)// + .setFollowRedirects(true)// + .setConnectionTimeout(this.connectionTimeout) + .setReadTimeout(this.readTimeout) + .contentType(getXmlContentType())// + .body(getMsgStr(false))// + .executeAsync(); + } + + /** + * 获取请求的Content-Type,附加编码信息 + * + * @return 请求的Content-Type + */ + private String getXmlContentType() { + return TEXT_XML_CONTENT_TYPE.concat(this.charset.toString()); + } + + /** + * 设置方法参数 + * + * @param ele 方法节点 + * @param name 参数名 + * @param value 参数值 + * @param prefix 命名空间前缀 + * @return {@link SOAPElement}子节点 + */ + @SuppressWarnings("rawtypes") + private static SOAPElement setParam(SOAPElement ele, String name, Object value, String prefix) { + final SOAPElement childEle; + try { + if (StrUtil.isNotBlank(prefix)) { + childEle = ele.addChildElement(name, prefix); + } else { + childEle = ele.addChildElement(name); + } + } catch (SOAPException e) { + throw new SoapRuntimeException(e); + } + + if(null != value) { + if (value instanceof SOAPElement) { + // 单个子节点 + try { + ele.addChildElement((SOAPElement) value); + } catch (SOAPException e) { + throw new SoapRuntimeException(e); + } + } else if (value instanceof Map) { + // 多个字节点 + Entry entry; + for (Object obj : ((Map) value).entrySet()) { + entry = (Entry) obj; + setParam(childEle, entry.getKey().toString(), entry.getValue(), prefix); + } + } else { + // 单个值 + childEle.setValue(value.toString()); + } + } + + return childEle; + } + // -------------------------------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-http/src/main/java/cn/hutool/http/webservice/SoapProtocol.java b/hutool-http/src/main/java/cn/hutool/http/webservice/SoapProtocol.java new file mode 100644 index 000000000..2c890221f --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/webservice/SoapProtocol.java @@ -0,0 +1,36 @@ +package cn.hutool.http.webservice; + +import javax.xml.soap.SOAPConstants; + +/** + * SOAP协议版本枚举 + * + * @author looly + * + */ +public enum SoapProtocol { + /** SOAP 1.1协议 */ + SOAP_1_1(SOAPConstants.SOAP_1_1_PROTOCOL), + /** SOAP 1.2协议 */ + SOAP_1_2(SOAPConstants.SOAP_1_2_PROTOCOL); + + /** + * 构造 + * + * @param value {@link SOAPConstants} 中的协议版本值 + */ + private SoapProtocol(String value) { + this.value = value; + } + + private String value; + + /** + * 获取版本值信息 + * + * @return 版本值信息 + */ + public String getValue() { + return this.value; + } +} diff --git a/hutool-http/src/main/java/cn/hutool/http/webservice/SoapRuntimeException.java b/hutool-http/src/main/java/cn/hutool/http/webservice/SoapRuntimeException.java new file mode 100644 index 000000000..947fd5382 --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/webservice/SoapRuntimeException.java @@ -0,0 +1,32 @@ +package cn.hutool.http.webservice; + +import cn.hutool.core.util.StrUtil; + +/** + * SOAP异常 + * + * @author xiaoleilu + */ +public class SoapRuntimeException extends RuntimeException { + private static final long serialVersionUID = 8247610319171014183L; + + public SoapRuntimeException(Throwable e) { + super(e.getMessage(), e); + } + + public SoapRuntimeException(String message) { + super(message); + } + + public SoapRuntimeException(String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params)); + } + + public SoapRuntimeException(String message, Throwable throwable) { + super(message, throwable); + } + + public SoapRuntimeException(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } +} diff --git a/hutool-http/src/main/java/cn/hutool/http/webservice/SoapUtil.java b/hutool-http/src/main/java/cn/hutool/http/webservice/SoapUtil.java new file mode 100644 index 000000000..4aba5e739 --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/webservice/SoapUtil.java @@ -0,0 +1,92 @@ +package cn.hutool.http.webservice; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; + +import javax.xml.soap.SOAPException; +import javax.xml.soap.SOAPMessage; + +import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.XmlUtil; + +/** + * SOAP相关工具类 + * + * @author looly + * @since 4.5.7 + */ +public class SoapUtil { + + /** + * 创建SOAP客户端,默认使用soap1.1版本协议 + * + * @param url WS的URL地址 + * @return {@link SoapClient} + */ + public static SoapClient createClient(String url) { + return SoapClient.create(url); + } + + /** + * 创建SOAP客户端 + * + * @param url WS的URL地址 + * @param protocol 协议,见{@link SoapProtocol} + * @return {@link SoapClient} + */ + public static SoapClient createClient(String url, SoapProtocol protocol) { + return SoapClient.create(url, protocol); + } + + /** + * 创建SOAP客户端 + * + * @param url WS的URL地址 + * @param protocol 协议,见{@link SoapProtocol} + * @param namespaceURI 方法上的命名空间URI + * @return {@link SoapClient} + * @since 4.5.6 + */ + public static SoapClient createClient(String url, SoapProtocol protocol, String namespaceURI) { + return SoapClient.create(url, protocol, namespaceURI); + } + + /** + * {@link SOAPMessage} 转为字符串 + * + * @param message SOAP消息对象 + * @param pretty 是否格式化 + * @return SOAP XML字符串 + */ + public static String toString(SOAPMessage message, boolean pretty) { + return toString(message, pretty, CharsetUtil.CHARSET_UTF_8); + } + + /** + * {@link SOAPMessage} 转为字符串 + * + * @param message SOAP消息对象 + * @param pretty 是否格式化 + * @param charset 编码 + * @return SOAP XML字符串 + * @since 4.5.7 + */ + public static String toString(SOAPMessage message, boolean pretty, Charset charset) { + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + message.writeTo(out); + } catch (SOAPException | IOException e) { + throw new SoapRuntimeException(e); + } + String messageToString = null; + try { + messageToString = out.toString(charset.toString()); + } catch (UnsupportedEncodingException e) { + throw new UtilException(e); + } + return pretty ? XmlUtil.format(messageToString) : messageToString; + } +} diff --git a/hutool-http/src/main/java/cn/hutool/http/webservice/package-info.java b/hutool-http/src/main/java/cn/hutool/http/webservice/package-info.java new file mode 100644 index 000000000..c37934c93 --- /dev/null +++ b/hutool-http/src/main/java/cn/hutool/http/webservice/package-info.java @@ -0,0 +1,7 @@ +/** + * Webservice客户端封装实现 + * + * @author looly + * + */ +package cn.hutool.http.webservice; \ No newline at end of file diff --git a/hutool-http/src/test/java/cn/hutool/http/test/DownloadTest.java b/hutool-http/src/test/java/cn/hutool/http/test/DownloadTest.java new file mode 100644 index 000000000..8cf664711 --- /dev/null +++ b/hutool-http/src/test/java/cn/hutool/http/test/DownloadTest.java @@ -0,0 +1,66 @@ +package cn.hutool.http.test; + +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.StreamProgress; +import cn.hutool.core.lang.Console; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpUtil; + +/** + * 下载单元测试 + * + * @author looly + */ +public class DownloadTest { + + @Test + @Ignore + public void downloadPicTest() { + String url = "http://wx.qlogo.cn/mmopen/vKhlFcibVUtNBVDjcIowlg0X8aJfHXrTNCEFBukWVH9ta99pfEN88lU39MKspCUCOP3yrFBH3y2NbV7sYtIIlon8XxLwAEqv2/0"; + HttpUtil.downloadFile(url, "e:/pic/t3.jpg"); + Console.log("ok"); + } + + @Test + @Ignore + public void downloadSizeTest() { + String url = "https://res.t-io.org/im/upload/img/67/8948/1119501/88097554/74541310922/85/231910/366466 - 副本.jpg"; + HttpRequest.get(url).setSSLProtocol("TLSv1.2").executeAsync().writeBody("e:/pic/366466.jpg"); + } + + @Test + @Ignore + public void downloadTest1() { + long size = HttpUtil.downloadFile("http://explorer.bbfriend.com/crossdomain.xml", "e:/temp/"); + System.out.println("Download size: " + size); + } + + @Test + @Ignore + public void downloadTest() { + // 带进度显示的文件下载 + HttpUtil.downloadFile("http://mirrors.sohu.com/centos/7/isos/x86_64/CentOS-7-x86_64-DVD-1810.iso", FileUtil.file("d:/"), new StreamProgress() { + + long time = System.currentTimeMillis(); + + @Override + public void start() { + Console.log("开始下载。。。。"); + } + + @Override + public void progress(long progressSize) { + long speed = progressSize / (System.currentTimeMillis() - time) * 1000; + Console.log("已下载:{}, 速度:{}/s", FileUtil.readableFileSize(progressSize), FileUtil.readableFileSize(speed)); + } + + @Override + public void finish() { + Console.log("下载完成!"); + } + }); + } +} diff --git a/hutool-http/src/test/java/cn/hutool/http/test/HtmlUtilTest.java b/hutool-http/src/test/java/cn/hutool/http/test/HtmlUtilTest.java new file mode 100644 index 000000000..8175beb0c --- /dev/null +++ b/hutool-http/src/test/java/cn/hutool/http/test/HtmlUtilTest.java @@ -0,0 +1,96 @@ +package cn.hutool.http.test; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.http.HtmlUtil; + +/** + * Html单元测试 + * + * @author looly + * + */ +public class HtmlUtilTest { + + @Test + public void removeHtmlTagTest() { + //非闭合标签 + String str = "pre"; + String result = HtmlUtil.removeHtmlTag(str, "img"); + Assert.assertEquals("pre", result); + + //闭合标签 + str = "pre"; + result = HtmlUtil.removeHtmlTag(str, "img"); + Assert.assertEquals("pre", result); + + //闭合标签 + str = "pre"; + result = HtmlUtil.removeHtmlTag(str, "img"); + Assert.assertEquals("pre", result); + + //闭合标签 + str = "pre"; + result = HtmlUtil.removeHtmlTag(str, "img"); + Assert.assertEquals("pre", result); + + //包含内容标签 + str = "pre
dfdsfdsfdsf
"; + result = HtmlUtil.removeHtmlTag(str, "div"); + Assert.assertEquals("pre", result); + + //带换行 + str = "pre
\r\n\t\tdfdsfdsfdsf\r\n
"; + result = HtmlUtil.removeHtmlTag(str, "div"); + Assert.assertEquals("pre", result); + } + + @Test + public void unwrapHtmlTagTest() { + //非闭合标签 + String str = "pre"; + String result = HtmlUtil.unwrapHtmlTag(str, "img"); + Assert.assertEquals("pre", result); + + //闭合标签 + str = "pre"; + result = HtmlUtil.unwrapHtmlTag(str, "img"); + Assert.assertEquals("pre", result); + + //闭合标签 + str = "pre"; + result = HtmlUtil.unwrapHtmlTag(str, "img"); + Assert.assertEquals("pre", result); + + //闭合标签 + str = "pre"; + result = HtmlUtil.unwrapHtmlTag(str, "img"); + Assert.assertEquals("pre", result); + + //包含内容标签 + str = "pre
abc
"; + result = HtmlUtil.unwrapHtmlTag(str, "div"); + Assert.assertEquals("preabc", result); + + //带换行 + str = "pre
\r\n\t\tabc\r\n
"; + result = HtmlUtil.unwrapHtmlTag(str, "div"); + Assert.assertEquals("pre\r\n\t\tabc\r\n", result); + } + + @Test + public void escapeTest() { + String html = "123'123'"; + String escape = HtmlUtil.escape(html); + String restoreEscaped = HtmlUtil.unescape(escape); + Assert.assertEquals(html, restoreEscaped); + } + + @Test + public void filterTest() { + String html = ""; + String filter = HtmlUtil.filter(html); + Assert.assertEquals("", filter); + } +} diff --git a/hutool-http/src/test/java/cn/hutool/http/test/HttpRequestTest.java b/hutool-http/src/test/java/cn/hutool/http/test/HttpRequestTest.java new file mode 100644 index 000000000..917803b8f --- /dev/null +++ b/hutool-http/src/test/java/cn/hutool/http/test/HttpRequestTest.java @@ -0,0 +1,92 @@ +package cn.hutool.http.test; + +import java.util.List; +import java.util.Map; + +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.date.TimeInterval; +import cn.hutool.core.lang.Console; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.ssl.SSLSocketFactoryBuilder; + +/** + * {@link HttpRequest}单元测试 + * + * @author Looly + * + */ +public class HttpRequestTest { + final String url = "http://photo.qzone.qq.com/fcgi-bin/fcg_list_album?uin=88888&outstyle=2"; + + @Test + @Ignore + public void getHttpsTest() { + String body = HttpRequest.get("https://www.gjifa.com/pc/").execute().body(); + Console.log(body); + } + + @Test + @Ignore + public void getWithParamsTest() { + String url = "http://gc.ditu.aliyun.com/geocoding?ccc=你好"; + + HttpRequest request = HttpRequest.get(url).setEncodeUrlParams(true).body("a=乌海"); + String body = request.execute().body(); + Console.log(body); + +// String body2 = HttpUtil.get(url); +// Console.log(body2); + } + + @Test + @Ignore + public void asyncHeadTest() { + HttpResponse response = HttpRequest.head(url).execute(); + Map> headers = response.headers(); + Console.log(headers); + Console.log(response.body()); + } + + @Test + @Ignore + public void asyncGetTest() { + TimeInterval timer = DateUtil.timer(); + HttpResponse body = HttpRequest.get(url).charset("GBK").executeAsync(); + long interval = timer.interval(); + timer.restart(); + Console.log(body.body()); + long interval2 = timer.interval(); + Console.log("Async response spend {}ms, body spend {}ms", interval, interval2); + } + + @Test + @Ignore + public void syncGetTest() { + TimeInterval timer = DateUtil.timer(); + HttpResponse body = HttpRequest.get(url).charset("GBK").execute(); + long interval = timer.interval(); + timer.restart(); + Console.log(body.body()); + long interval2 = timer.interval(); + Console.log("Async response spend {}ms, body spend {}ms", interval, interval2); + } + + @Test + @Ignore + public void customGetTest() { + // 自定义构建HTTP GET请求,发送Http GET请求,针对HTTPS安全加密,可以自定义SSL + HttpRequest request = HttpRequest.get(url) + // 自定义返回编码 + .charset(CharsetUtil.CHARSET_GBK) + // 禁用缓存 + .disableCache() + // 自定义SSL版本 + .setSSLProtocol(SSLSocketFactoryBuilder.TLSv12); + Console.log(request.execute().body()); + } +} diff --git a/hutool-http/src/test/java/cn/hutool/http/test/HttpUtilTest.java b/hutool-http/src/test/java/cn/hutool/http/test/HttpUtilTest.java new file mode 100644 index 000000000..ac061f1b2 --- /dev/null +++ b/hutool-http/src/test/java/cn/hutool/http/test/HttpUtilTest.java @@ -0,0 +1,265 @@ +package cn.hutool.http.test; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.lang.Console; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.ReUtil; +import cn.hutool.http.Header; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpUtil; + +public class HttpUtilTest { + + @Test + @Ignore + public void postTest() { + String result = HttpUtil.createPost("api.uhaozu.com/goods/description/1120448506").charset(CharsetUtil.UTF_8).execute().body(); + Console.log(result); + } + + @Test + @Ignore + public void getTest() { + String result1 = HttpUtil.get("http://photo.qzone.qq.com/fcgi-bin/fcg_list_album?uin=88888&outstyle=2", CharsetUtil.CHARSET_GBK); + Console.log(result1); + } + + @Test + @Ignore + public void getTest2() { + // 自定义的默认header无效 + String result = HttpRequest + .get("https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=101457313&redirect_uri=http%3A%2F%2Fwww.benmovip.com%2Fpay-cloud%2Fqqlogin%2FgetCode&state=ok") + .removeHeader(Header.USER_AGENT).execute().body(); + Console.log(result); + } + + @Test + @Ignore + public void getTest3() { + // 测试url中带有空格的情况 + String result1 = HttpUtil.get("http://122.152.198.206:5000/kf?abc= d"); + Console.log(result1); + } + + @Test + @Ignore + public void getTest4() { + // 测试url中带有空格的情况 + byte[] str = HttpRequest.get("http://img01.fs.yiban.cn/mobile/2D0Y71").execute().bodyBytes(); + + FileUtil.writeBytes(str, "f:/test/2D.jpg"); + Console.log(str); + } + + @Test + @Ignore + public void getTest5() { + String res = HttpUtil.get("https://comment.bilibili.com/67573272.xml"); + Console.log(res); + } + + @Test + @Ignore + public void get12306Test() { + String result = HttpUtil.get("https://kyfw.12306.cn/otn/"); + Console.log(result); + } + + @Test + @Ignore + public void downloadStringTest() { + String url = "https://www.baidu.com"; + // 从远程直接读取字符串,需要自定义编码,直接调用JDK方法 + String content2 = HttpUtil.downloadString(url, CharsetUtil.UTF_8); + Console.log(content2); + } + + @Test + @Ignore + public void oschinaTest() { + // 请求列表页 + String listContent = HttpUtil.get("https://www.oschina.net/action/ajax/get_more_news_list?newsType=&p=2"); + // 使用正则获取所有标题 + List titles = ReUtil.findAll("(.*?)", listContent, 1); + for (String title : titles) { + // 打印标题 + Console.log(title); + } + + // 请求下一页,检查Cookie是否复用 + listContent = HttpUtil.get("https://www.oschina.net/action/ajax/get_more_news_list?newsType=&p=3"); + } + + @Test + public void decodeParamsTest() { + String paramsStr = "uuuu=0&a=b&c=%3F%23%40!%24%25%5E%26%3Ddsssss555555"; + Map> map = HttpUtil.decodeParams(paramsStr, CharsetUtil.UTF_8); + Assert.assertEquals("0", map.get("uuuu").get(0)); + Assert.assertEquals("b", map.get("a").get(0)); + Assert.assertEquals("?#@!$%^&=dsssss555555", map.get("c").get(0)); + } + + @Test + public void toParamsTest() { + String paramsStr = "uuuu=0&a=b&c=3Ddsssss555555"; + Map> map = HttpUtil.decodeParams(paramsStr, CharsetUtil.UTF_8); + + String encodedParams = HttpUtil.toParams((Map>) map); + Assert.assertEquals(paramsStr, encodedParams); + } + + @Test + public void encodeParamTest() { + // ?单独存在去除之,&单位位于末尾去除之 + String paramsStr = "?a=b&c=d&"; + String encode = HttpUtil.encodeParams(paramsStr, CharsetUtil.CHARSET_UTF_8); + Assert.assertEquals("a=b&c=d", encode); + + // url不参与转码 + paramsStr = "http://www.abc.dd?a=b&c=d&"; + encode = HttpUtil.encodeParams(paramsStr, CharsetUtil.CHARSET_UTF_8); + Assert.assertEquals("http://www.abc.dd?a=b&c=d", encode); + + // b=b中的=被当作值的一部分,不做encode + paramsStr = "a=b=b&c=d&"; + encode = HttpUtil.encodeParams(paramsStr, CharsetUtil.CHARSET_UTF_8); + Assert.assertEquals("a=b=b&c=d", encode); + + // =d的情况被处理为key为空 + paramsStr = "a=bbb&c=d&=d"; + encode = HttpUtil.encodeParams(paramsStr, CharsetUtil.CHARSET_UTF_8); + Assert.assertEquals("a=bbb&c=d&=d", encode); + + // d=的情况被处理为value为空 + paramsStr = "a=bbb&c=d&d="; + encode = HttpUtil.encodeParams(paramsStr, CharsetUtil.CHARSET_UTF_8); + Assert.assertEquals("a=bbb&c=d&d=", encode); + + // 多个&&被处理为单个,相当于空条件 + paramsStr = "a=bbb&c=d&&&d="; + encode = HttpUtil.encodeParams(paramsStr, CharsetUtil.CHARSET_UTF_8); + Assert.assertEquals("a=bbb&c=d&d=", encode); + + // &d&相当于只有键,无值得情况 + paramsStr = "a=bbb&c=d&d&"; + encode = HttpUtil.encodeParams(paramsStr, CharsetUtil.CHARSET_UTF_8); + Assert.assertEquals("a=bbb&c=d&d=", encode); + + // 中文的键和值被编码 + paramsStr = "a=bbb&c=你好&哈喽&"; + encode = HttpUtil.encodeParams(paramsStr, CharsetUtil.CHARSET_UTF_8); + Assert.assertEquals("a=bbb&c=%E4%BD%A0%E5%A5%BD&%E5%93%88%E5%96%BD=", encode); + } + + @Test + public void decodeParamTest() { + // 开头的?被去除 + String a = "?a=b&c=d&"; + Map> map = HttpUtil.decodeParams(a, CharsetUtil.UTF_8); + Assert.assertEquals("b", map.get("a").get(0)); + Assert.assertEquals("d", map.get("c").get(0)); + + // =e被当作空为key,e为value + a = "?a=b&c=d&=e"; + map = HttpUtil.decodeParams(a, CharsetUtil.UTF_8); + Assert.assertEquals("b", map.get("a").get(0)); + Assert.assertEquals("d", map.get("c").get(0)); + Assert.assertEquals("e", map.get("").get(0)); + + // 多余的&去除 + a = "?a=b&c=d&=e&&&&"; + map = HttpUtil.decodeParams(a, CharsetUtil.UTF_8); + Assert.assertEquals("b", map.get("a").get(0)); + Assert.assertEquals("d", map.get("c").get(0)); + Assert.assertEquals("e", map.get("").get(0)); + + // 值为空 + a = "?a=b&c=d&e="; + map = HttpUtil.decodeParams(a, CharsetUtil.UTF_8); + Assert.assertEquals("b", map.get("a").get(0)); + Assert.assertEquals("d", map.get("c").get(0)); + Assert.assertEquals("", map.get("e").get(0)); + + // &=被作为键和值都为空 + a = "a=b&c=d&="; + map = HttpUtil.decodeParams(a, CharsetUtil.UTF_8); + Assert.assertEquals("b", map.get("a").get(0)); + Assert.assertEquals("d", map.get("c").get(0)); + Assert.assertEquals("", map.get("").get(0)); + + // &e&这类单独的字符串被当作key + a = "a=b&c=d&e&"; + map = HttpUtil.decodeParams(a, CharsetUtil.UTF_8); + Assert.assertEquals("b", map.get("a").get(0)); + Assert.assertEquals("d", map.get("c").get(0)); + Assert.assertEquals("", map.get("e").get(0)); + + // 被编码的键和值被还原 + a = "a=bbb&c=%E4%BD%A0%E5%A5%BD&%E5%93%88%E5%96%BD="; + map = HttpUtil.decodeParams(a, CharsetUtil.UTF_8); + Assert.assertEquals("bbb", map.get("a").get(0)); + Assert.assertEquals("你好", map.get("c").get(0)); + Assert.assertEquals("", map.get("哈喽").get(0)); + } + + @Test + @Ignore + public void patchTest() { + String body = HttpRequest.post("https://www.baidu.com").execute().body(); + Console.log(body); + } + + @Test + public void urlWithFormTest() { + Map param = new LinkedHashMap<>(); + param.put("AccessKeyId", "123"); + param.put("Action", "DescribeDomainRecords"); + param.put("Format", "date"); + param.put("DomainName", "lesper.cn"); // 域名地址 + param.put("SignatureMethod", "POST"); + param.put("SignatureNonce", "123"); + param.put("SignatureVersion", "4.3.1"); + param.put("Timestamp", 123432453); + param.put("Version", "1.0"); + + String urlWithForm = HttpUtil.urlWithForm("http://api.hutool.cn/login?type=aaa", param, CharsetUtil.CHARSET_UTF_8, false); + Assert.assertEquals( + "http://api.hutool.cn/login?type=aaa&AccessKeyId=123&Action=DescribeDomainRecords&Format=date&DomainName=lesper.cn&SignatureMethod=POST&SignatureNonce=123&SignatureVersion=4.3.1&Timestamp=123432453&Version=1.0", + urlWithForm); + + urlWithForm = HttpUtil.urlWithForm("http://api.hutool.cn/login?type=aaa", param, CharsetUtil.CHARSET_UTF_8, false); + Assert.assertEquals( + "http://api.hutool.cn/login?type=aaa&AccessKeyId=123&Action=DescribeDomainRecords&Format=date&DomainName=lesper.cn&SignatureMethod=POST&SignatureNonce=123&SignatureVersion=4.3.1&Timestamp=123432453&Version=1.0", + urlWithForm); + } + + @Test + public void getCharsetTest() { + String charsetName = ReUtil.get(HttpUtil.CHARSET_PATTERN, "Charset=UTF-8;fq=0.9", 1); + Assert.assertEquals("UTF-8", charsetName); + + charsetName = ReUtil.get(HttpUtil.META_CHARSET_PATTERN, " paramMap = new HashMap<>(); + paramMap.put("city", "北京"); + paramMap.put("file", file); + String result = HttpUtil.post("http://wthrcdn.etouch.cn/weather_mini", paramMap); + System.out.println(result); + } +} diff --git a/hutool-http/src/test/java/cn/hutool/http/useragent/UserAgentUtilTest.java b/hutool-http/src/test/java/cn/hutool/http/useragent/UserAgentUtilTest.java new file mode 100644 index 000000000..8aa6911e3 --- /dev/null +++ b/hutool-http/src/test/java/cn/hutool/http/useragent/UserAgentUtilTest.java @@ -0,0 +1,151 @@ +package cn.hutool.http.useragent; + +import org.junit.Assert; +import org.junit.Test; + +public class UserAgentUtilTest { + + @Test + public void parseDesktopTest() { + String uaStr = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.163 Safari/535.1"; + + UserAgent ua = UserAgentUtil.parse(uaStr); + Assert.assertEquals("Chrome", ua.getBrowser().toString()); + Assert.assertEquals("14.0.835.163", ua.getVersion()); + Assert.assertEquals("Webkit", ua.getEngine().toString()); + Assert.assertEquals("535.1", ua.getEngineVersion()); + Assert.assertEquals("Windows 7 or Windows Server 2008R2", ua.getOs().toString()); + Assert.assertEquals("Windows", ua.getPlatform().toString()); + Assert.assertFalse(ua.isMobile()); + } + + @Test + public void parseMobileTest() { + String uaStr = "User-Agent:Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5"; + + UserAgent ua = UserAgentUtil.parse(uaStr); + Assert.assertEquals("Safari", ua.getBrowser().toString()); + Assert.assertEquals("5.0.2", ua.getVersion()); + Assert.assertEquals("Webkit", ua.getEngine().toString()); + Assert.assertEquals("533.17.9", ua.getEngineVersion()); + Assert.assertEquals("iPhone", ua.getOs().toString()); + Assert.assertEquals("iPhone", ua.getPlatform().toString()); + Assert.assertTrue(ua.isMobile()); + } + + @Test + public void parseMiui10WithChromeTest(){ + String uaStr="Mozilla/5.0 (Linux; Android 9; MIX 3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36"; + UserAgent ua=UserAgentUtil.parse(uaStr); + Assert.assertEquals("Chrome", ua.getBrowser().toString()); + Assert.assertEquals("70.0.3538.80", ua.getVersion()); + Assert.assertEquals("Webkit", ua.getEngine().toString()); + Assert.assertEquals("537.36", ua.getEngineVersion()); + Assert.assertEquals("Android", ua.getOs().toString()); + Assert.assertEquals("Android", ua.getPlatform().toString()); + Assert.assertTrue(ua.isMobile()); + } + + @Test + public void parseWindows10WithChromeTest(){ + String uaStr="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36"; + UserAgent ua=UserAgentUtil.parse(uaStr); + Assert.assertEquals("Chrome", ua.getBrowser().toString()); + Assert.assertEquals("70.0.3538.102", ua.getVersion()); + Assert.assertEquals("Webkit", ua.getEngine().toString()); + Assert.assertEquals("537.36", ua.getEngineVersion()); + Assert.assertEquals("Windows 10 or Windows Server 2016", ua.getOs().toString()); + Assert.assertEquals("Windows", ua.getPlatform().toString()); + Assert.assertFalse(ua.isMobile()); + } + + @Test + public void parseWindows10WithIe11Test(){ + String uaStr="Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko"; + UserAgent ua=UserAgentUtil.parse(uaStr); + Assert.assertEquals("MSIE11", ua.getBrowser().toString()); + Assert.assertEquals("11.0", ua.getVersion()); + Assert.assertEquals("Trident", ua.getEngine().toString()); + Assert.assertEquals("7.0", ua.getEngineVersion()); + Assert.assertEquals("Windows 10 or Windows Server 2016", ua.getOs().toString()); + Assert.assertEquals("Windows", ua.getPlatform().toString()); + Assert.assertFalse(ua.isMobile()); + } + + @Test + public void parseWindows10WithIeMobileLumia520Test(){ + String uaStr="Mozilla/5.0 (Mobile; Windows Phone 8.1; Android 4.0; ARM; Trident/7.0; Touch; rv:11.0; IEMobile/11.0; NOKIA; Lumia 520) like iPhone OS 7_0_3 Mac OS X AppleWebKit/537 (KHTML, like Gecko) Mobile Safari/537 "; + UserAgent ua=UserAgentUtil.parse(uaStr); + Assert.assertEquals("IEMobile", ua.getBrowser().toString()); + Assert.assertEquals("11.0", ua.getVersion()); + Assert.assertEquals("Trident", ua.getEngine().toString()); + Assert.assertEquals("7.0", ua.getEngineVersion()); + Assert.assertEquals("Windows Phone", ua.getOs().toString()); + Assert.assertEquals("Windows Phone", ua.getPlatform().toString()); + Assert.assertTrue(ua.isMobile()); + } + + @Test + public void parseWindows10WithIe8EmulatorTest(){ + String uaStr="Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0)"; + UserAgent ua=UserAgentUtil.parse(uaStr); + Assert.assertEquals("MSIE", ua.getBrowser().toString()); + Assert.assertEquals("8.0", ua.getVersion()); + Assert.assertEquals("Trident", ua.getEngine().toString()); + Assert.assertEquals("4.0", ua.getEngineVersion()); + Assert.assertEquals("Windows 7 or Windows Server 2008R2", ua.getOs().toString()); + Assert.assertEquals("Windows", ua.getPlatform().toString()); + Assert.assertFalse(ua.isMobile()); + } + + @Test + public void parseWindows10WithEdgeTest(){ + String uaStr="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/18.17763"; + UserAgent ua=UserAgentUtil.parse(uaStr); + Assert.assertEquals("MSEdge", ua.getBrowser().toString()); + Assert.assertEquals("18.17763", ua.getVersion()); + Assert.assertEquals("Webkit", ua.getEngine().toString()); + Assert.assertEquals("537.36", ua.getEngineVersion()); + Assert.assertEquals("Windows 10 or Windows Server 2016", ua.getOs().toString()); + Assert.assertEquals("Windows", ua.getPlatform().toString()); + Assert.assertFalse(ua.isMobile()); + } + @Test + public void parseEdgeOnLumia950XLTest(){ + String uaStr="Mozilla/5.0 (Windows Phone 10.0; Android 6.0.1; Microsoft; Lumia 950XL) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Mobile Safari/537.36 Edge/15.14900"; + UserAgent ua=UserAgentUtil.parse(uaStr); + Assert.assertEquals("MSEdge", ua.getBrowser().toString()); + Assert.assertEquals("15.14900", ua.getVersion()); + Assert.assertEquals("Webkit", ua.getEngine().toString()); + Assert.assertEquals("537.36", ua.getEngineVersion()); + Assert.assertEquals("Windows Phone", ua.getOs().toString()); + Assert.assertEquals("Windows Phone", ua.getPlatform().toString()); + Assert.assertTrue(ua.isMobile()); + } + + @Test + public void parseChromeOnWindowsServer2012R2Test(){ + String uaStr="Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"; + UserAgent ua=UserAgentUtil.parse(uaStr); + Assert.assertEquals("Chrome", ua.getBrowser().toString()); + Assert.assertEquals("63.0.3239.132", ua.getVersion()); + Assert.assertEquals("Webkit", ua.getEngine().toString()); + Assert.assertEquals("537.36", ua.getEngineVersion()); + Assert.assertEquals("Windows 8.1 or Winsows Server 2012R2", ua.getOs().toString()); + Assert.assertEquals("Windows", ua.getPlatform().toString()); + Assert.assertFalse(ua.isMobile()); + } + + @Test + public void parseIE11OnWindowsServer2008R2Test(){ + String uaStr="Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko"; + UserAgent ua=UserAgentUtil.parse(uaStr); + Assert.assertEquals("MSIE11", ua.getBrowser().toString()); + Assert.assertEquals("11.0", ua.getVersion()); + Assert.assertEquals("Trident", ua.getEngine().toString()); + Assert.assertEquals("7.0", ua.getEngineVersion()); + Assert.assertEquals("Windows 7 or Windows Server 2008R2", ua.getOs().toString()); + Assert.assertEquals("Windows", ua.getPlatform().toString()); + Assert.assertFalse(ua.isMobile()); + } +} diff --git a/hutool-http/src/test/java/cn/hutool/http/webservice/SoapClientTest.java b/hutool-http/src/test/java/cn/hutool/http/webservice/SoapClientTest.java new file mode 100644 index 000000000..b52da7d11 --- /dev/null +++ b/hutool-http/src/test/java/cn/hutool/http/webservice/SoapClientTest.java @@ -0,0 +1,44 @@ +package cn.hutool.http.webservice; + +import javax.xml.soap.SOAPException; +import javax.xml.soap.SOAPMessage; + +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.lang.Console; +import cn.hutool.core.util.CharsetUtil; + +/** + * SOAP相关单元测试 + * + * @author looly + * + */ +public class SoapClientTest { + + @Test + @Ignore + public void requestTest() { + SoapClient client = SoapClient.create("http://www.webxml.com.cn/WebServices/IpAddressSearchWebService.asmx") + .setMethod("web:getCountryCityByIp", "http://WebXml.com.cn/") + .setCharset(CharsetUtil.CHARSET_GBK) + .setParam("theIpAddress", "218.21.240.106"); + + Console.log(client.getMsgStr(true)); + + Console.log(client.send(true)); + } + + @Test + @Ignore + public void requestForMessageTest() throws SOAPException { + SoapClient client = SoapClient.create("http://www.webxml.com.cn/WebServices/IpAddressSearchWebService.asmx") + .setMethod("web:getCountryCityByIp", "http://WebXml.com.cn/") + .setParam("theIpAddress", "218.21.240.106"); + + SOAPMessage message = client.sendForMessage(); + Console.log(message.getSOAPBody().getTextContent()); + } + +} diff --git a/hutool-json/pom.xml b/hutool-json/pom.xml new file mode 100644 index 000000000..f4569163b --- /dev/null +++ b/hutool-json/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + + jar + + + cn.hutool + hutool-parent + 4.6.2-SNAPSHOT + + + hutool-json + ${project.artifactId} + Hutool JSON封装 + + + + cn.hutool + hutool-core + ${project.parent.version} + + + org.projectlombok + lombok + 1.18.6 + test + + + diff --git a/hutool-json/src/main/java/cn/hutool/json/InternalJSONUtil.java b/hutool-json/src/main/java/cn/hutool/json/InternalJSONUtil.java new file mode 100644 index 000000000..2697a57cd --- /dev/null +++ b/hutool-json/src/main/java/cn/hutool/json/InternalJSONUtil.java @@ -0,0 +1,241 @@ +package cn.hutool.json; + +import java.io.IOException; +import java.io.Writer; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; +import java.util.Iterator; +import java.util.Map; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 内部JSON工具类,仅用于JSON内部使用 + * + * @author Looly + * + */ +final class InternalJSONUtil { + + private InternalJSONUtil() { + } + + /** + * 写入值到Writer + * + * @param writer Writer + * @param value 值 + * @param indentFactor 每一级别的缩进量 + * @param indent 缩进空格数 + * @param config 配置项 + * @return Writer + * @throws JSONException + * @throws IOException + */ + protected static final Writer writeValue(Writer writer, Object value, int indentFactor, int indent, JSONConfig config) throws JSONException, IOException { + if (value == null || value instanceof JSONNull) { + writer.write(JSONNull.NULL.toString()); + } else if (value instanceof JSON) { + ((JSON) value).write(writer, indentFactor, indent); + } else if (value instanceof Map) { + new JSONObject((Map) value).write(writer, indentFactor, indent); + } else if (value instanceof Iterable || value instanceof Iterator || value.getClass().isArray()) { + new JSONArray(value).write(writer, indentFactor, indent); + } else if (value instanceof Number) { + writer.write(NumberUtil.toStr((Number) value)); + } else if (value instanceof Date || value instanceof Calendar) { + final String format = (null == config) ? null : config.getDateFormat(); + writer.write(formatDate(value, format)); + } else if (value instanceof Boolean) { + writer.write(value.toString()); + } else if (value instanceof JSONString) { + Object o; + try { + o = ((JSONString) value).toJSONString(); + } catch (Exception e) { + throw new JSONException(e); + } + writer.write(o != null ? o.toString() : JSONUtil.quote(value.toString())); + } else { + JSONUtil.quote(value.toString(), writer); + } + return writer; + } + + /** + * 缩进,使用空格符 + * + * @param writer + * @param indent + * @throws IOException + */ + protected static final void indent(Writer writer, int indent) throws IOException { + for (int i = 0; i < indent; i += 1) { + writer.write(CharUtil.SPACE); + } + } + + /** + * 如果对象是Number 且是 NaN or infinite,将抛出异常 + * + * @param obj 被检查的对象 + * @throws JSONException If o is a non-finite number. + */ + protected static void testValidity(Object obj) throws JSONException { + if (false == ObjectUtil.isValidIfNumber(obj)) { + throw new JSONException("JSON does not allow non-finite numbers."); + } + } + + /** + * 值转为String,用于JSON中。 If the object has an value.toJSONString() method, then that method will be used to produce the JSON text.
+ * The method is required to produce a strictly conforming text.
+ * If the object does not contain a toJSONString method (which is the most common case), then a text will be produced by other means.
+ * If the value is an array or Collection, then a JSONArray will be made from it and its toJSONString method will be called.
+ * If the value is a MAP, then a JSONObject will be made from it and its toJSONString method will be called.
+ * Otherwise, the value's toString method will be called, and the result will be quoted.
+ * + * @param value 需要转为字符串的对象 + * @return 字符串 + * @throws JSONException If the value is or contains an invalid number. + */ + protected static String valueToString(Object value) throws JSONException { + if (value == null || value instanceof JSONNull) { + return "null"; + } + if (value instanceof JSONString) { + try { + return ((JSONString) value).toJSONString(); + } catch (Exception e) { + throw new JSONException(e); + } + } else if (value instanceof Number) { + return NumberUtil.toStr((Number) value); + } else if (value instanceof Boolean || value instanceof JSONObject || value instanceof JSONArray) { + return value.toString(); + } else if (value instanceof Map) { + Map map = (Map) value; + return new JSONObject(map).toString(); + } else if (value instanceof Collection) { + Collection coll = (Collection) value; + return new JSONArray(coll).toString(); + } else if (value.getClass().isArray()) { + return new JSONArray(value).toString(); + } else { + return JSONUtil.quote(value.toString()); + } + } + + /** + * 尝试转换字符串为number, boolean, or null,无法转换返回String + * + * @param string A String. + * @return A simple JSON value. + */ + protected static Object stringToValue(String string) { + Double d; + if (null == string || "null".equalsIgnoreCase(string)) { + return JSONNull.NULL; + } + + if (StrUtil.EMPTY.equals(string)) { + return string; + } + if ("true".equalsIgnoreCase(string)) { + return Boolean.TRUE; + } + if ("false".equalsIgnoreCase(string)) { + return Boolean.FALSE; + } + + /* If it might be a number, try converting it. If a number cannot be produced, then the value will just be a string. */ + char b = string.charAt(0); + if ((b >= '0' && b <= '9') || b == '-') { + try { + if (string.indexOf('.') > -1 || string.indexOf('e') > -1 || string.indexOf('E') > -1) { + d = Double.valueOf(string); + if (!d.isInfinite() && !d.isNaN()) { + return d; + } + } else { + Long myLong = new Long(string); + if (string.equals(myLong.toString())) { + if (myLong == myLong.intValue()) { + return myLong.intValue(); + } else { + return myLong; + } + } + } + } catch (Exception ignore) { + } + } + return string; + } + + /** + * 将Property的键转化为JSON形式
+ * 用于识别类似于:com.luxiaolei.package.hutool这类用点隔开的键 + * + * @param jsonObject JSONObject + * @param key 键 + * @param value 值 + * @return JSONObject + */ + protected static JSONObject propertyPut(JSONObject jsonObject, Object key, Object value) { + String keyStr = Convert.toStr(key); + String[] path = StrUtil.split(keyStr, StrUtil.DOT); + int last = path.length - 1; + JSONObject target = jsonObject; + for (int i = 0; i < last; i += 1) { + String segment = path[i]; + JSONObject nextTarget = target.getJSONObject(segment); + if (nextTarget == null) { + nextTarget = new JSONObject(); + target.put(segment, nextTarget); + } + target = nextTarget; + } + target.put(path[last], value); + return jsonObject; + } + + /** + * 默认情况下是否忽略null值的策略选择
+ * JavaBean默认忽略null值,其它对象不忽略 + * + * @param obj 需要检查的对象 + * @return 是否忽略null值 + * @since 4.3.1 + */ + protected static boolean defaultIgnoreNullValue(Object obj) { + if(obj instanceof CharSequence || obj instanceof JSONTokener || obj instanceof Map) { + return false; + } + return true; + } + + /** + * 按照给定格式格式化日期,格式为空时返回时间戳字符串 + * + * @param dateObj Date或者Calendar对象 + * @param format 格式 + * @return 日期字符串 + */ + private static String formatDate(Object dateObj, String format) { + if (StrUtil.isNotBlank(format)) { + final Date date = (dateObj instanceof Date) ? (Date)dateObj : ((Calendar)dateObj).getTime(); + //用户定义了日期格式 + return JSONUtil.quote(DateUtil.format(date, format)); + } + + //默认使用时间戳 + return String.valueOf((dateObj instanceof Date) ? ((Date)dateObj).getTime() : ((Calendar)dateObj).getTimeInMillis()); + } +} diff --git a/hutool-json/src/main/java/cn/hutool/json/JSON.java b/hutool-json/src/main/java/cn/hutool/json/JSON.java new file mode 100644 index 000000000..8b981377b --- /dev/null +++ b/hutool-json/src/main/java/cn/hutool/json/JSON.java @@ -0,0 +1,128 @@ +package cn.hutool.json; + +import java.io.Serializable; +import java.io.Writer; + +import cn.hutool.core.bean.BeanPath; + +/** + * JSON接口 + * + * @author Looly + * + */ +public interface JSON extends Cloneable, Serializable{ + + /** + * 通过表达式获取JSON中嵌套的对象
+ *
    + *
  1. .表达式,可以获取Bean对象中的属性(字段)值或者Map中key对应的值
  2. + *
  3. []表达式,可以获取集合等对象中对应index的值
  4. + *
+ * + * 表达式栗子: + * + *
+	 * persion
+	 * persion.name
+	 * persons[3]
+	 * person.friends[5].name
+	 * 
+ * + * @param expression 表达式 + * @return 对象 + * @see BeanPath#get(Object) + * @since 4.0.6 + */ + public Object getByPath(String expression); + + /** + * 设置表达式指定位置(或filed对应)的值
+ * 若表达式指向一个JSONArray则设置其坐标对应位置的值,若指向JSONObject则put对应key的值
+ * 注意:如果为JSONArray,设置值下标小于其长度,将替换原有值,否则追加新值
+ *
    + *
  1. .表达式,可以获取Bean对象中的属性(字段)值或者Map中key对应的值
  2. + *
  3. []表达式,可以获取集合等对象中对应index的值
  4. + *
+ * + * 表达式栗子: + * + *
+	 * persion
+	 * persion.name
+	 * persons[3]
+	 * person.friends[5].name
+	 * 
+ * + * @param expression 表达式 + * @param value 值 + */ + public void putByPath(String expression, Object value); + + /** + * 通过表达式获取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 4.0.6 + */ + public T getByPath(String expression, Class resultType); + + /** + * 将JSON内容写入Writer,无缩进
+ * Warning: This method assumes that the data structure is acyclical. + * + * @param writer Writer + * @return Writer + * @throws JSONException JSON相关异常 + */ + public Writer write(Writer writer) throws JSONException; + + /** + * 将JSON内容写入Writer
+ * Warning: This method assumes that the data structure is acyclical. + * + * @param writer writer + * @param indentFactor 缩进因子,定义每一级别增加的缩进量 + * @param indent 本级别缩进量 + * @return Writer + * @throws JSONException JSON相关异常 + */ + public Writer write(Writer writer, int indentFactor, int indent) throws JSONException; + + /** + * 转换为JSON字符串 + * + * @param indentFactor 缩进因子,定义每一级别增加的缩进量 + * @return JSON字符串 + * @throws JSONException JSON相关异常 + */ + public String toJSONString(int indentFactor) throws JSONException; + + /** + * 格式化打印JSON,缩进为4个空格 + * + * @return 格式化后的JSON字符串 + * @throws JSONException 包含非法数抛出此异常 + * @since 3.0.9 + */ + public String toStringPretty() throws JSONException; +} diff --git a/hutool-json/src/main/java/cn/hutool/json/JSONArray.java b/hutool-json/src/main/java/cn/hutool/json/JSONArray.java new file mode 100644 index 000000000..379375e23 --- /dev/null +++ b/hutool-json/src/main/java/cn/hutool/json/JSONArray.java @@ -0,0 +1,628 @@ +package cn.hutool.json; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.RandomAccess; + +import cn.hutool.core.bean.BeanPath; +import cn.hutool.core.collection.ArrayIter; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; + +/** + * JSON数组
+ * JSON数组是表示中括号括住的数据表现形式
+ * 对应的JSON字符串格格式例如: + * + *
+ * ["a", "b", "c", 12]
+ * 
+ * + * @author looly + */ +public class JSONArray extends JSONGetter implements JSON, List, RandomAccess { + private static final long serialVersionUID = 2664900568717612292L; + + /** 默认初始大小 */ + private static final int DEFAULT_CAPACITY = 10; + + /** 持有原始数据的List */ + private final List rawList; + /** 配置项 */ + private JSONConfig config; + + // -------------------------------------------------------------------------------------------------------------------- Constructor start + /** + * 构造
+ * 默认使用{@link ArrayList} 实现 + */ + public JSONArray() { + this(DEFAULT_CAPACITY); + } + + /** + * 构造
+ * 默认使用{@link ArrayList} 实现 + * + * @param initialCapacity 初始大小 + * @since 3.2.2 + */ + public JSONArray(int initialCapacity) { + this(initialCapacity, JSONConfig.create()); + } + + /** + * 构造
+ * 默认使用{@link ArrayList} 实现 + * + * @param initialCapacity 初始大小 + * @param config JSON配置项 + * @since 4.1.19 + */ + public JSONArray(int initialCapacity, JSONConfig config) { + this.rawList = new ArrayList(initialCapacity); + this.config = config; + } + + /** + * 构造
+ * 将参数数组中的元素转换为JSON对应的对象加入到JSONArray中 + * + * @param list 初始化的JSON数组 + */ + public JSONArray(Iterable list) { + this(); + for (Object o : list) { + this.add(o); + } + } + + /** + * 构造
+ * 将参数数组中的元素转换为JSON对应的对象加入到JSONArray中 + * + * @param list 初始化的JSON数组 + */ + public JSONArray(Collection list) { + this(list.size()); + for (Object o : list) { + this.add(o); + } + } + + /** + * 使用 {@link JSONTokener} 做为参数构造 + * + * @param x A {@link JSONTokener} + * @throws JSONException If there is a syntax error. + */ + public JSONArray(JSONTokener x) throws JSONException { + this(); + init(x); + } + + /** + * 从String构造(JSONArray字符串) + * + * @param source JSON数组字符串 + * @throws JSONException If there is a syntax error. + */ + public JSONArray(CharSequence source) throws JSONException { + this(); + init(source); + } + + /** + * 从对象构造,忽略{@code null}的值
+ * 支持以下类型的参数: + * + *
+	 * 1. 数组
+	 * 2. {@link Iterable}对象
+	 * 3. JSON数组字符串
+	 * 
+ * + * @param object 数组或集合或JSON数组字符串 + * @throws JSONException 非数组或集合 + */ + public JSONArray(Object object) throws JSONException { + this(object, true); + } + + /** + * 从对象构造
+ * 支持以下类型的参数: + * + *
+	 * 1. 数组
+	 * 2. {@link Iterable}对象
+	 * 3. JSON数组字符串
+	 * 
+ * + * @param object 数组或集合或JSON数组字符串 + * @param ignoreNullValue 是否忽略空值 + * @throws JSONException 非数组或集合 + */ + public JSONArray(Object object, boolean ignoreNullValue) throws JSONException { + this(DEFAULT_CAPACITY, JSONConfig.create().setIgnoreNullValue(ignoreNullValue)); + init(object); + } + // -------------------------------------------------------------------------------------------------------------------- Constructor start + + /** + * 设置转为字符串时的日期格式,默认为时间戳(null值) + * + * @param format 格式,null表示使用时间戳 + * @return this + * @since 4.1.19 + */ + public JSONArray setDateFormat(String format) { + this.config.setDateFormat(format); + return this; + } + + /** + * JSONArray转为以separator为分界符的字符串 + * + * @param separator 分界符 + * @return a string. + * @throws JSONException If the array contains an invalid number. + */ + public String join(String separator) throws JSONException { + int len = this.rawList.size(); + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < len; i += 1) { + if (i > 0) { + sb.append(separator); + } + sb.append(InternalJSONUtil.valueToString(this.rawList.get(i))); + } + return sb.toString(); + } + + @Override + public Object get(int index) { + return this.rawList.get(index); + } + + @Override + public Object getObj(Integer index, Object defaultValue) { + return (index < 0 || index >= this.size()) ? defaultValue : this.rawList.get(index); + } + + @Override + public Object getByPath(String expression) { + return BeanPath.create(expression).get(this); + } + + @Override + public T getByPath(String expression, Class resultType) { + return JSONConverter.jsonConvert(resultType, getByPath(expression), true); + } + + @Override + public void putByPath(String expression, Object value) { + BeanPath.create(expression).set(this, value); + } + + /** + * Append an object value. This increases the array's length by one.
+ * 加入元素,数组长度+1,等同于 {@link JSONArray#add(Object)} + * + * @param value 值,可以是: Boolean, Double, Integer, JSONArray, JSONObject, Long, or String, or the JSONNull.NULL。 + * @return this. + */ + public JSONArray put(Object value) { + this.add(value); + return this; + } + + /** + * 加入或者替换JSONArray中指定Index的值,如果index大于JSONArray的长度,将在指定index设置值,之前的位置填充JSONNull.Null + * + * @param index 位置 + * @param value 值对象. 可以是以下类型: Boolean, Double, Integer, JSONArray, JSONObject, Long, String, or the JSONNull.NULL. + * @return this. + * @throws JSONException index < 0 或者非有限的数字 + */ + public JSONArray put(int index, Object value) throws JSONException { + this.add(index, value); + return this; + } + + /** + * 根据给定名列表,与其位置对应的值组成JSONObject + * + * @param names 名列表,位置与JSONArray中的值位置对应 + * @return A JSONObject,无名或值返回null + * @throws JSONException 如果任何一个名为null + */ + public JSONObject toJSONObject(JSONArray names) throws JSONException { + if (names == null || names.size() == 0 || this.size() == 0) { + return null; + } + JSONObject jo = new JSONObject(); + for (int i = 0; i < names.size(); i += 1) { + jo.put(names.getStr(i), this.getObj(i)); + } + return jo; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((rawList == null) ? 0 : rawList.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 JSONArray other = (JSONArray) obj; + if (rawList == null) { + if (other.rawList != null) { + return false; + } + } else if (!rawList.equals(other.rawList)) { + return false; + } + return true; + } + + @Override + public Iterator iterator() { + return rawList.iterator(); + } + + /** + * 当此JSON列表的每个元素都是一个JSONObject时,可以调用此方法返回一个Iterable,便于使用foreach语法遍历 + * + * @return Iterable + * @since 4.0.12 + */ + public Iterable jsonIter() { + return new JSONObjectIter(iterator()); + } + + @Override + public int size() { + return rawList.size(); + } + + @Override + public boolean isEmpty() { + return rawList.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return rawList.contains(o); + } + + @Override + public Object[] toArray() { + return rawList.toArray(); + } + + @Override + @SuppressWarnings("unchecked") + public T[] toArray(T[] a) { + return (T[]) JSONConverter.toArray(this, a.getClass().getComponentType()); + } + + @Override + public boolean add(Object e) { + return this.rawList.add(JSONUtil.wrap(e, this.config.isIgnoreNullValue())); + } + + @Override + public Object remove(int index) { + return index >= 0 && index < this.size() ? this.rawList.remove(index) : null; + } + + @Override + public boolean remove(Object o) { + return rawList.remove(o); + } + + @Override + public boolean containsAll(Collection c) { + return rawList.containsAll(c); + } + + @Override + public boolean addAll(Collection c) { + if (CollUtil.isEmpty(c)) { + return false; + } + for (Object obj : c) { + this.add(obj); + } + return true; + } + + @Override + public boolean addAll(int index, Collection c) { + if (CollUtil.isEmpty(c)) { + return false; + } + final ArrayList list = new ArrayList<>(c.size()); + for (Object object : c) { + list.add(JSONUtil.wrap(object, this.config.isIgnoreNullValue())); + } + return rawList.addAll(index, list); + } + + @Override + public boolean removeAll(Collection c) { + return this.rawList.removeAll(c); + } + + @Override + public boolean retainAll(Collection c) { + return this.rawList.retainAll(c); + } + + @Override + public void clear() { + this.rawList.clear(); + + } + + @Override + public Object set(int index, Object element) { + return this.rawList.set(index, JSONUtil.wrap(element, this.config.isIgnoreNullValue())); + } + + @Override + public void add(int index, Object element) { + if (index < 0) { + throw new JSONException("JSONArray[" + index + "] not found."); + } + if (index < this.size()) { + InternalJSONUtil.testValidity(element); + this.rawList.add(index, JSONUtil.wrap(element, this.config.isIgnoreNullValue())); + } else { + while (index != this.size()) { + this.add(JSONNull.NULL); + } + this.put(element); + } + + } + + @Override + public int indexOf(Object o) { + return this.rawList.indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + return this.rawList.lastIndexOf(o); + } + + @Override + public ListIterator listIterator() { + return this.rawList.listIterator(); + } + + @Override + public ListIterator listIterator(int index) { + return this.rawList.listIterator(index); + } + + @Override + public List subList(int fromIndex, int toIndex) { + return this.rawList.subList(fromIndex, toIndex); + } + + /** + * 转为Bean数组 + * + * @param arrayClass 数组元素类型 + * @return 实体类对象 + */ + public Object toArray(Class arrayClass) { + return JSONConverter.toArray(this, arrayClass); + } + + /** + * 转为{@link ArrayList} + * + * @param 元素类型 + * @param elementType 元素类型 + * @return {@link ArrayList} + * @since 3.0.8 + */ + public List toList(Class elementType) { + return JSONConverter.toList(this, elementType); + } + + /** + * 转为JSON字符串,无缩进 + * + * @return JSONArray字符串 + */ + @Override + public String toString() { + try { + return this.toJSONString(0); + } catch (Exception e) { + return null; + } + } + + /** + * 格式化打印JSON,缩进为4个空格 + * + * @return 格式化后的JSON字符串 + * @throws JSONException 包含非法数抛出此异常 + * @since 3.0.9 + */ + @Override + public String toStringPretty() throws JSONException { + return this.toJSONString(4); + } + + /** + * 转为JSON字符串,指定缩进值 + * + * @param indentFactor 缩进值,既缩进空格数 + * @return JSON字符串 + * @throws JSONException JSON写入异常 + */ + @Override + public String toJSONString(int indentFactor) throws JSONException { + StringWriter sw = new StringWriter(); + synchronized (sw.getBuffer()) { + return this.write(sw, indentFactor, 0).toString(); + } + } + + @Override + public Writer write(Writer writer) throws JSONException { + return this.write(writer, 0, 0); + } + + @Override + public Writer write(Writer writer, int indentFactor, int indent) throws JSONException { + try { + return doWrite(writer, indentFactor, indent); + } catch (IOException e) { + throw new JSONException(e); + } + } + + // ------------------------------------------------------------------------------------------------- Private method start + + /** + * 将JSON内容写入Writer + * + * @param writer writer + * @param indentFactor 缩进因子,定义每一级别增加的缩进量 + * @param indent 本级别缩进量 + * @return Writer + * @throws IOException IO相关异常 + */ + private Writer doWrite(Writer writer, int indentFactor, int indent) throws IOException { + writer.write(CharUtil.BRACKET_START); + final int newindent = indent + indentFactor; + final boolean isIgnoreNullValue = this.config.isIgnoreNullValue(); + boolean isFirst = true; + for (Object obj : this.rawList) { + if(ObjectUtil.isNull(obj) && isIgnoreNullValue) { + continue; + } + if (isFirst) { + isFirst = false; + }else { + writer.write(CharUtil.COMMA); + } + + if (indentFactor > 0) { + writer.write(CharUtil.LF); + } + InternalJSONUtil.indent(writer, newindent); + InternalJSONUtil.writeValue(writer, obj, indentFactor, newindent, this.config); + } + + if (indentFactor > 0) { + writer.write(CharUtil.LF); + } + InternalJSONUtil.indent(writer, indent); + writer.write(CharUtil.BRACKET_END); + return writer; + } + + /** + * 初始化 + * + * @param object 数组或集合或JSON数组字符串 + * @throws JSONException 非数组或集合 + */ + private void init(Object object) throws JSONException{ + if (object instanceof CharSequence) { + // JSON字符串 + init((CharSequence) object); + } else { + Iterator iter; + if (object.getClass().isArray()) {// 数组 + iter = new ArrayIter<>(object); + } else if (object instanceof Iterator) {// Iterator + iter = ((Iterator) object); + } else if (object instanceof Iterable) {// Iterable + iter = ((Iterable) object).iterator(); + } else { + throw new JSONException("JSONArray initial value should be a string or collection or array."); + } + while (iter.hasNext()) { + this.add(iter.next()); + } + } + } + + /** + * 初始化 + * + * @param source JSON字符串 + */ + private void init(CharSequence source) { + if (null != source) { + init(new JSONTokener(StrUtil.trim(source))); + } + } + + /** + * 初始化 + * + * @param x {@link JSONTokener} + */ + private void init(JSONTokener x) { + if (x.nextClean() != '[') { + throw x.syntaxError("A JSONArray text must start with '['"); + } + if (x.nextClean() != ']') { + x.back(); + for (;;) { + if (x.nextClean() == ',') { + x.back(); + this.rawList.add(JSONNull.NULL); + } else { + x.back(); + this.rawList.add(x.nextValue()); + } + switch (x.nextClean()) { + case ',': + if (x.nextClean() == ']') { + return; + } + x.back(); + break; + case ']': + return; + default: + throw x.syntaxError("Expected a ',' or ']'"); + } + } + } + } + // ------------------------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-json/src/main/java/cn/hutool/json/JSONConfig.java b/hutool-json/src/main/java/cn/hutool/json/JSONConfig.java new file mode 100644 index 000000000..9b6ba3a31 --- /dev/null +++ b/hutool-json/src/main/java/cn/hutool/json/JSONConfig.java @@ -0,0 +1,132 @@ +package cn.hutool.json; + +import java.io.Serializable; + +/** + * JSON配置项 + * + * @author looly + * @since 4.1.19 + */ +public class JSONConfig implements Serializable { + private static final long serialVersionUID = 119730355204738278L; + + /** 是否有序,顺序按照加入顺序排序 */ + private boolean order; + /** 是否忽略转换过程中的异常 */ + private boolean ignoreError; + /** 是否忽略键的大小写 */ + private boolean ignoreCase; + /** 日期格式,null表示默认的时间戳 */ + private String dateFormat; + /** 是否忽略null值 */ + private boolean ignoreNullValue = true; + + /** + * 创建默认的配置项 + * @return JSONConfig + */ + public static JSONConfig create() { + return new JSONConfig(); + } + + /** + * 是否有序,顺序按照加入顺序排序 + * + * @return 是否有序 + */ + public boolean isOrder() { + return order; + } + + /** + * 设置是否有序,顺序按照加入顺序排序 + * + * @param order 是否有序 + * @return this + */ + public JSONConfig setOrder(boolean order) { + this.order = order; + return this; + } + + /** + * 是否忽略转换过程中的异常 + * + * @return 是否忽略转换过程中的异常 + */ + public boolean isIgnoreError() { + return ignoreError; + } + + /** + * 设置是否忽略转换过程中的异常 + * + * @param ignoreError 是否忽略转换过程中的异常 + * @return this + */ + public JSONConfig setIgnoreError(boolean ignoreError) { + this.ignoreError = ignoreError; + return this; + } + + /** + * 是否忽略键的大小写 + * + * @return 是否忽略键的大小写 + */ + public boolean isIgnoreCase() { + return ignoreCase; + } + + /** + * 设置是否忽略键的大小写 + * + * @param ignoreCase 是否忽略键的大小写 + * @return this + */ + public JSONConfig setIgnoreCase(boolean ignoreCase) { + this.ignoreCase = ignoreCase; + return this; + } + + /** + * 日期格式,null表示默认的时间戳 + * + * @return 日期格式,null表示默认的时间戳 + */ + public String getDateFormat() { + return dateFormat; + } + + /** + * 设置日期格式,null表示默认的时间戳 + * + * @param dateFormat 日期格式,null表示默认的时间戳 + * @return this + */ + public JSONConfig setDateFormat(String dateFormat) { + this.dateFormat = dateFormat; + return this; + } + + /** + * 是否忽略null值 + * + * @return 是否忽略null值 + */ + public boolean isIgnoreNullValue() { + return this.ignoreNullValue; + } + + /** + * 设置是否忽略null值 + * + * @param ignoreNullValue 是否忽略null值 + * @return this + */ + public JSONConfig setIgnoreNullValue(boolean ignoreNullValue) { + this.ignoreNullValue = ignoreNullValue; + return this; + } +} diff --git a/hutool-json/src/main/java/cn/hutool/json/JSONConverter.java b/hutool-json/src/main/java/cn/hutool/json/JSONConverter.java new file mode 100644 index 000000000..8ad9bd2da --- /dev/null +++ b/hutool-json/src/main/java/cn/hutool/json/JSONConverter.java @@ -0,0 +1,99 @@ +package cn.hutool.json; + +import java.lang.reflect.Type; +import java.util.List; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.convert.ConvertException; +import cn.hutool.core.convert.Converter; +import cn.hutool.core.convert.ConverterRegistry; +import cn.hutool.core.convert.impl.ArrayConverter; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.TypeUtil; + +/** + * JSON转换器 + * + * @author looly + * @since 4.2.2 + */ +public class JSONConverter implements Converter { + + static { + // 注册到转换中心 + ConverterRegistry registry = ConverterRegistry.getInstance(); + registry.putCustom(JSON.class, JSONConverter.class); + registry.putCustom(JSONObject.class, JSONConverter.class); + registry.putCustom(JSONArray.class, JSONConverter.class); + } + + /** + * JSONArray转数组 + * + * @param jsonArray JSONArray + * @param arrayClass 数组元素类型 + * @return 数组对象 + */ + protected static Object toArray(JSONArray jsonArray, Class arrayClass) { + return new ArrayConverter(arrayClass).convert(jsonArray, null); + } + + /** + * 将JSONArray转换为指定类型的对量列表 + * + * @param 元素类型 + * @param jsonArray JSONArray + * @param elementType 对象元素类型 + * @return 对象列表 + */ + protected static List toList(JSONArray jsonArray, Class elementType) { + return Convert.toList(elementType, jsonArray); + } + + /** + * JSON递归转换
+ * 首先尝试JDK类型转换,如果失败尝试JSON转Bean + * @param + * + * @param targetType 目标类型 + * @param value 值 + * @param ignoreError 是否忽略转换错误 + * @return 目标类型的值 + * @throws ConvertException 转换失败 + */ + @SuppressWarnings("unchecked") + protected static T jsonConvert(Type targetType, Object value, boolean ignoreError) throws ConvertException { + if (JSONUtil.isNull(value)) { + return null; + } + + Object targetValue = null; + try { + targetValue = Convert.convert(targetType, value); + } catch (ConvertException e) { + if (ignoreError) { + return null; + } + throw e; + } + + if (null == targetValue && false == ignoreError) { + if (StrUtil.isBlankIfStr(value)) { + // 对于传入空字符串的情况,如果转换的目标对象是非字符串或非原始类型,转换器会返回false。 + // 此处特殊处理,认为返回null属于正常情况 + return null; + } + + throw new ConvertException("Can not convert {} to type {}", value, ObjectUtil.defaultIfNull(TypeUtil.getClass(targetType), targetType)); + } + + return (T) targetValue; + } + + @Override + public JSON convert(Object value, JSON defaultValue) throws IllegalArgumentException { + return JSONUtil.parse(value); + } + +} diff --git a/hutool-json/src/main/java/cn/hutool/json/JSONException.java b/hutool-json/src/main/java/cn/hutool/json/JSONException.java new file mode 100644 index 000000000..e063fcc76 --- /dev/null +++ b/hutool-json/src/main/java/cn/hutool/json/JSONException.java @@ -0,0 +1,34 @@ +package cn.hutool.json; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.StrUtil; + +/** + * JSON异常 + * + * @author looly + * @since 3.0.2 + */ +public class JSONException extends RuntimeException { + private static final long serialVersionUID = 0; + + public JSONException(Throwable e) { + super(ExceptionUtil.getMessage(e), e); + } + + public JSONException(String message) { + super(message); + } + + public JSONException(String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params)); + } + + public JSONException(String message, Throwable cause) { + super(message, cause); + } + + public JSONException(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } +} diff --git a/hutool-json/src/main/java/cn/hutool/json/JSONGetter.java b/hutool-json/src/main/java/cn/hutool/json/JSONGetter.java new file mode 100644 index 000000000..d7b91723a --- /dev/null +++ b/hutool-json/src/main/java/cn/hutool/json/JSONGetter.java @@ -0,0 +1,133 @@ +package cn.hutool.json; + +import cn.hutool.core.convert.ConvertException; +import cn.hutool.core.getter.OptNullBasicTypeFromObjectGetter; + +/** + * 用于JSON的Getter类,提供各种类型的Getter方法 + * @author Looly + * + * @param Key类型 + */ +public abstract class JSONGetter extends OptNullBasicTypeFromObjectGetter{ + + /** + * key对应值是否为null或无此key + * + * @param key 键 + * @return true 无此key或值为null或{@link JSONNull#NULL}返回false,其它返回true + */ + public boolean isNull(K key) { + return JSONNull.NULL.equals(this.getObj(key)); + } + + /** + * 获取字符串类型值,并转义不可见字符,如'\n'换行符会被转义为字符串"\n" + * + * @param key 键 + * @return 字符串类型值 + * @since 4.2.2 + */ + public String getStrEscaped(K key) { + return getStrEscaped(key, null); + } + + /** + * 获取字符串类型值,并转义不可见字符,如'\n'换行符会被转义为字符串"\n" + * + * @param key 键 + * @param defaultValue 默认值 + * @return 字符串类型值 + * @since 4.2.2 + */ + public String getStrEscaped(K key, String defaultValue) { + return JSONUtil.escape(getStr(key, defaultValue)); + } + + /** + * 获得JSONArray对象
+ * 如果值为其它类型对象,尝试转换为{@link JSONArray}返回,否则抛出异常 + * + * @param key KEY + * @return JSONArray对象,如果值为null或者非JSONArray类型,返回null + */ + public JSONArray getJSONArray(K key) { + final Object object = this.getObj(key); + if(null == object) { + return null; + } + + if(object instanceof JSONArray) { + return (JSONArray) object; + } + return new JSONArray(object); + } + + /** + * 获得JSONObject对象
+ * 如果值为其它类型对象,尝试转换为{@link JSONObject}返回,否则抛出异常 + * + * @param key KEY + * @return JSONArray对象,如果值为null或者非JSONObject类型,返回null + */ + public JSONObject getJSONObject(K key) { + final Object object = this.getObj(key); + if(null == object) { + return null; + } + + if(object instanceof JSONObject) { + return (JSONObject) object; + } + return new JSONObject(object); + } + + /** + * 从JSON中直接获取Bean对象
+ * 先获取JSONObject对象,然后转为Bean对象 + * + * @param Bean类型 + * @param key KEY + * @param beanType Bean类型 + * @return Bean对象,如果值为null或者非JSONObject类型,返回null + * @since 3.1.1 + */ + public T getBean(K key, Class beanType) { + final JSONObject obj = getJSONObject(key); + return (null == obj) ? null : obj.toBean(beanType); + } + + /** + * 获取指定类型的对象
+ * 转换失败或抛出异常 + * + * @param 获取的对象类型 + * @param key 键 + * @param type 获取对象类型 + * @return 对象 + * @throws ConvertException 转换异常 + * @since 3.0.8 + */ + public T get(K key, Class type) throws ConvertException{ + return get(key, type, false); + } + + /** + * 获取指定类型的对象 + * + * @param 获取的对象类型 + * @param key 键 + * @param type 获取对象类型 + * @param ignoreError 是否跳过转换失败的对象或值 + * @return 对象 + * @throws ConvertException 转换异常 + * @since 3.0.8 + */ + public T get(K key, Class type, boolean ignoreError) throws ConvertException{ + final Object value = this.getObj(key); + if(null == value){ + return null; + } + return JSONConverter.jsonConvert(type, value, ignoreError); + } +} diff --git a/hutool-json/src/main/java/cn/hutool/json/JSONNull.java b/hutool-json/src/main/java/cn/hutool/json/JSONNull.java new file mode 100644 index 000000000..9991ce16c --- /dev/null +++ b/hutool-json/src/main/java/cn/hutool/json/JSONNull.java @@ -0,0 +1,59 @@ +package cn.hutool.json; + +import java.io.Serializable; + +/** + * 用于定义null,与Javascript中null相对应
+ * Java中的null值在js中表示为undefined。 + * @author Looly + * + */ +public class JSONNull implements Serializable{ + private static final long serialVersionUID = 2633815155870764938L; + + /** + * NULL 对象用于减少歧义来表示Java 中的null
+ * NULL.equals(null) 返回 true.
+ * NULL.toString() 返回 "null". + */ + public static final JSONNull NULL = new JSONNull(); + + /** + * There is only intended to be a single instance of the NULL object, so the clone method returns itself. + *克隆方法只返回本身,此对象是个单例对象 + * + * @return NULL. + */ + @Override + protected final Object clone() { + return this; + } + + /** + * A Null object is equal to the null value and to itself. + * 对象与其本身和null值相等 + * + * @param object An object to test for nullness. + * @return true if the object parameter is the JSONObject.NULL object or null. + */ + @Override + public boolean equals(Object object) { + return object == null || object == this; + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + /** + * Get the "null" string value. + *获得“null”字符串 + * + * @return The string "null". + */ + @Override + public String toString() { + return "null"; + } +} \ No newline at end of file diff --git a/hutool-json/src/main/java/cn/hutool/json/JSONObject.java b/hutool-json/src/main/java/cn/hutool/json/JSONObject.java new file mode 100644 index 000000000..7ee46b3bc --- /dev/null +++ b/hutool-json/src/main/java/cn/hutool/json/JSONObject.java @@ -0,0 +1,803 @@ +package cn.hutool.json; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import cn.hutool.core.bean.BeanDesc.PropDesc; +import cn.hutool.core.bean.BeanPath; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.TypeReference; +import cn.hutool.core.map.CaseInsensitiveLinkedMap; +import cn.hutool.core.map.CaseInsensitiveMap; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; + +/** + * JSON对象
+ * 例:
+ * + *
+ * json = new JSONObject().put("JSON", "Hello, World!").toString();
+ * 
+ * + * @author looly + */ +public class JSONObject extends JSONGetter implements JSON, Map { + private static final long serialVersionUID = -330220388580734346L; + + /** 默认初始大小 */ + private static final int DEFAULT_CAPACITY = 16; + + /** JSON的KV持有Map */ + private final Map rawHashMap; + /** 配置项 */ + private JSONConfig config; + + // -------------------------------------------------------------------------------------------------------------------- Constructor start + /** + * 构造,初始容量为 {@link #DEFAULT_CAPACITY},KEY无序 + */ + public JSONObject() { + this(DEFAULT_CAPACITY, false); + } + + /** + * 构造,初始容量为 {@link #DEFAULT_CAPACITY} + * + * @param isOrder 是否有序 + * @since 3.0.9 + */ + public JSONObject(boolean isOrder) { + this(DEFAULT_CAPACITY, isOrder); + } + + /** + * 构造 + * + * @param capacity 初始大小 + * @param isOrder 是否有序 + * @since 3.0.9 + */ + public JSONObject(int capacity, boolean isOrder) { + this(capacity, false, isOrder); + } + + /** + * 构造 + * + * @param capacity 初始大小 + * @param isIgnoreCase 是否忽略KEY大小写 + * @param isOrder 是否有序 + * @since 3.3.1 + */ + public JSONObject(int capacity, boolean isIgnoreCase, boolean isOrder) { + this(capacity, JSONConfig.create().setIgnoreCase(isIgnoreCase).setOrder(isOrder)); + } + + /** + * 构造 + * + * @param capacity 初始大小 + * @param config JSON配置项 + * @since 4.1.19 + */ + public JSONObject(int capacity, JSONConfig config) { + if (config.isIgnoreCase()) { + this.rawHashMap = config.isOrder() ? new CaseInsensitiveLinkedMap(capacity) : new CaseInsensitiveMap(capacity); + } else { + this.rawHashMap = config.isOrder() ? new LinkedHashMap(capacity) : new HashMap(capacity); + } + this.config = config; + } + + /** + * 构建JSONObject,JavaBean默认忽略null值,其它对象不忽略,规则如下: + *
    + *
  1. value为Map,将键值对加入JSON对象
  2. + *
  3. value为JSON字符串(CharSequence),使用JSONTokener解析
  4. + *
  5. value为JSONTokener,直接解析
  6. + *
  7. value为普通JavaBean,如果为普通的JavaBean,调用其getters方法(getXXX或者isXXX)获得值,加入到JSON对象。例如:如果JavaBean对象中有个方法getName(),值为"张三",获得的键值对为:name: "张三"
  8. + *
+ * + * @param source JavaBean或者Map对象或者String + */ + public JSONObject(Object source) { + this(source, InternalJSONUtil.defaultIgnoreNullValue(source)); + } + + /** + * 构建JSONObject,规则如下: + *
    + *
  1. value为Map,将键值对加入JSON对象
  2. + *
  3. value为JSON字符串(CharSequence),使用JSONTokener解析
  4. + *
  5. value为JSONTokener,直接解析
  6. + *
  7. value为普通JavaBean,如果为普通的JavaBean,调用其getters方法(getXXX或者isXXX)获得值,加入到JSON对象。例如:如果JavaBean对象中有个方法getName(),值为"张三",获得的键值对为:name: "张三"
  8. + *
+ * + * @param source JavaBean或者Map对象或者String + * @param ignoreNullValue 是否忽略空值 + * @since 3.0.9 + */ + public JSONObject(Object source, boolean ignoreNullValue) { + this(source, ignoreNullValue, (source instanceof LinkedHashMap)); + } + + /** + * 构建JSONObject,规则如下: + *
    + *
  1. value为Map,将键值对加入JSON对象
  2. + *
  3. value为JSON字符串(CharSequence),使用JSONTokener解析
  4. + *
  5. value为JSONTokener,直接解析
  6. + *
  7. value为普通JavaBean,如果为普通的JavaBean,调用其getters方法(getXXX或者isXXX)获得值,加入到JSON对象。例如:如果JavaBean对象中有个方法getName(),值为"张三",获得的键值对为:name: "张三"
  8. + *
+ * + * @param source JavaBean或者Map对象或者String + * @param ignoreNullValue 是否忽略空值,如果source为JSON字符串,不忽略空值 + * @param isOrder 是否有序 + * @since 4.2.2 + */ + public JSONObject(Object source, boolean ignoreNullValue, boolean isOrder) { + this(source, JSONConfig.create().setOrder(isOrder)// + .setIgnoreCase((source instanceof CaseInsensitiveMap) || (source instanceof CaseInsensitiveLinkedMap))// + .setIgnoreNullValue(ignoreNullValue)); + } + + /** + * 构建JSONObject,规则如下: + *
    + *
  1. value为Map,将键值对加入JSON对象
  2. + *
  3. value为JSON字符串(CharSequence),使用JSONTokener解析
  4. + *
  5. value为JSONTokener,直接解析
  6. + *
  7. value为普通JavaBean,如果为普通的JavaBean,调用其getters方法(getXXX或者isXXX)获得值,加入到JSON对象。例如:如果JavaBean对象中有个方法getName(),值为"张三",获得的键值对为:name: "张三"
  8. + *
+ * + * 如果给定值为Map,将键值对加入JSON对象;
+ * 如果为普通的JavaBean,调用其getters方法(getXXX或者isXXX)获得值,加入到JSON对象
+ * 例如:如果JavaBean对象中有个方法getName(),值为"张三",获得的键值对为:name: "张三" + * + * @param source JavaBean或者Map对象或者String + * @param config JSON配置文件 + * @since 4.2.2 + */ + public JSONObject(Object source, JSONConfig config) { + this(DEFAULT_CAPACITY, config); + init(source); + } + + /** + * 构建指定name列表对应的键值对为新的JSONObject,情况如下: + * + *
+	 * 1. 若obj为Map,则获取name列表对应键值对
+	 * 2. 若obj为普通Bean,使用反射方式获取字段名和字段值
+	 * 
+ * + * KEY或VALUE任意一个为null则不加入,字段不存在也不加入
+ * 若names列表为空,则字段全部加入 + * + * @param obj 包含需要字段的Bean对象或者Map对象 + * @param names 需要构建JSONObject的字段名列表 + */ + public JSONObject(Object obj, String... names) { + this(); + if (ArrayUtil.isEmpty(names)) { + init(obj); + return; + } + + if (obj instanceof Map) { + Object value; + for (String name : names) { + value = ((Map) obj).get(name); + this.putOnce(name, value); + } + } else { + for (String name : names) { + try { + this.putOpt(name, ReflectUtil.getFieldValue(obj, name)); + } catch (Exception ignore) { + // ignore + } + } + } + } + + /** + * 从JSON字符串解析为JSON对象,对于排序单独配置参数 + * + * @param source 以大括号 {} 包围的字符串,其中KEY和VALUE使用 : 分隔,每个键值对使用逗号分隔 + * @param isOrder 是否有序 + * @exception JSONException JSON字符串语法错误 + * @since 4.2.2 + */ + public JSONObject(CharSequence source, boolean isOrder) throws JSONException { + this(source, JSONConfig.create().setOrder(isOrder)); + } + + // -------------------------------------------------------------------------------------------------------------------- Constructor end + + /** + * 获取JSON配置 + * + * @return {@link JSONConfig} + * @since 4.3.1 + */ + public JSONConfig getConfig() { + return this.config; + } + + /** + * 设置转为字符串时的日期格式,默认为时间戳(null值) + * + * @param format 格式,null表示使用时间戳 + * @return this + * @since 4.1.19 + */ + public JSONObject setDateFormat(String format) { + this.config.setDateFormat(format); + return this; + } + + /** + * 将指定KEY列表的值组成新的JSONArray + * + * @param names KEY列表 + * @return A JSONArray of values. + * @throws JSONException If any of the values are non-finite numbers. + */ + public JSONArray toJSONArray(Collection names) throws JSONException { + if (CollectionUtil.isEmpty(names)) { + return null; + } + final JSONArray ja = new JSONArray(); + Object value; + for (String name : names) { + value = this.get(name); + if (null != value) { + ja.put(value); + } + } + return ja; + } + + /** + * 转为实体类对象,转换异常将被抛出 + * + * @param Bean类型 + * @param clazz 实体类 + * @return 实体类对象 + */ + public T toBean(Class clazz) { + return toBean((Type) clazz); + } + + /** + * 转为实体类对象,转换异常将被抛出 + * + * @param Bean类型 + * @param reference {@link TypeReference}类型参考子类,可以获取其泛型参数中的Type类型 + * @return 实体类对象 + * @since 4.2.2 + */ + public T toBean(TypeReference reference) { + return toBean(reference.getType()); + } + + /** + * 转为实体类对象 + * + * @param Bean类型 + * @param type {@link Type} + * @return 实体类对象 + * @since 3.0.8 + */ + public T toBean(Type type) { + return toBean(type, false); + } + + /** + * 转为实体类对象 + * + * @param Bean类型 + * @param type {@link Type} + * @param ignoreError 是否忽略转换错误 + * @return 实体类对象 + * @since 4.3.2 + */ + public T toBean(Type type, boolean ignoreError) { + return JSONConverter.jsonConvert(type, this, ignoreError); + } + + @Override + public int size() { + return rawHashMap.size(); + } + + @Override + public boolean isEmpty() { + return rawHashMap.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return rawHashMap.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return rawHashMap.containsValue(value); + } + + @Override + public Object get(Object key) { + return rawHashMap.get(key); + } + + @Override + public Object getObj(String key, Object defaultValue) { + Object obj = this.rawHashMap.get(key); + return null == obj ? defaultValue : obj; + } + + @Override + public Object getByPath(String expression) { + return BeanPath.create(expression).get(this); + } + + @Override + public T getByPath(String expression, Class resultType) { + return JSONConverter.jsonConvert(resultType, getByPath(expression), true); + } + + @Override + public void putByPath(String expression, Object value) { + BeanPath.create(expression).set(this, value); + } + + /** + * PUT 键值对到JSONObject中,如果值为null,将此键移除 + * + * @param key 键 + * @param value 值对象. 可以是以下类型: Boolean, Double, Integer, JSONArray, JSONObject, Long, String, or the JSONNull.NULL. + * @return this. + * @throws JSONException 值是无穷数字抛出此异常 + */ + @Override + public JSONObject put(String key, Object value) throws JSONException { + if (null == key) { + return this; + } + + final boolean ignoreNullValue = this.config.isIgnoreNullValue(); + if (ObjectUtil.isNull(value) && ignoreNullValue) { + // 忽略值模式下如果值为空清除key + this.remove(key); + } else { + InternalJSONUtil.testValidity(value); + this.rawHashMap.put(key, JSONUtil.wrap(value, ignoreNullValue)); + } + return this; + } + + /** + * 一次性Put 键值对,如果key已经存在抛出异常,如果键值中有null值,忽略 + * + * @param key 键 + * @param value 值对象,可以是以下类型: Boolean, Double, Integer, JSONArray, JSONObject, Long, String, or the JSONNull.NULL. + * @return this. + * @throws JSONException 值是无穷数字、键重复抛出异常 + */ + public JSONObject putOnce(String key, Object value) throws JSONException { + if (key != null && value != null) { + if (rawHashMap.containsKey(key)) { + throw new JSONException("Duplicate key \"{}\"", key); + } + this.put(key, value); + } + return this; + } + + /** + * 在键和值都为非空的情况下put到JSONObject中 + * + * @param key 键 + * @param value 值对象,可以是以下类型: Boolean, Double, Integer, JSONArray, JSONObject, Long, String, or the JSONNull.NULL. + * @return this. + * @throws JSONException 值是无穷数字 + */ + public JSONObject putOpt(String key, Object value) throws JSONException { + if (key != null && value != null) { + this.put(key, value); + } + return this; + } + + @Override + public void putAll(Map m) { + for (Entry entry : m.entrySet()) { + this.put(entry.getKey(), entry.getValue()); + } + } + + /** + * 积累值。类似于put,当key对应value已经存在时,与value组成新的JSONArray.
+ * 如果只有一个值,此值就是value,如果多个值,则是添加到新的JSONArray中 + * + * @param key 键 + * @param value 被积累的值 + * @return this. + * @throws JSONException 如果给定键为null或者键对应的值存在且为非JSONArray + */ + public JSONObject accumulate(String key, Object value) throws JSONException { + InternalJSONUtil.testValidity(value); + Object object = this.getObj(key); + if (object == null) { + this.put(key, value instanceof JSONArray ? new JSONArray().put(value) : value); + } else if (object instanceof JSONArray) { + ((JSONArray) object).put(value); + } else { + this.put(key, new JSONArray().put(object).put(value)); + } + return this; + } + + /** + * 追加值,如果key无对应值,就添加一个JSONArray,其元素只有value,如果值已经是一个JSONArray,则添加到值JSONArray中。 + * + * @param key 键 + * @param value 值 + * @return this. + * @throws JSONException 如果给定键为null或者键对应的值存在且为非JSONArray + */ + public JSONObject append(String key, Object value) throws JSONException { + InternalJSONUtil.testValidity(value); + Object object = this.getObj(key); + if (object == null) { + this.put(key, new JSONArray().put(value)); + } else if (object instanceof JSONArray) { + this.put(key, ((JSONArray) object).put(value)); + } else { + throw new JSONException("JSONObject [" + key + "] is not a JSONArray."); + } + return this; + } + + /** + * 对值加一,如果值不存在,赋值1,如果为数字类型,做加一操作 + * + * @param key A key string. + * @return this. + * @throws JSONException 如果存在值非Integer, Long, Double, 或 Float. + */ + public JSONObject increment(String key) throws JSONException { + Object value = this.getObj(key); + if (value == null) { + this.put(key, 1); + } else if (value instanceof BigInteger) { + this.put(key, ((BigInteger) value).add(BigInteger.ONE)); + } else if (value instanceof BigDecimal) { + this.put(key, ((BigDecimal) value).add(BigDecimal.ONE)); + } else if (value instanceof Integer) { + this.put(key, (Integer) value + 1); + } else if (value instanceof Long) { + this.put(key, (Long) value + 1); + } else if (value instanceof Double) { + this.put(key, (Double) value + 1); + } else if (value instanceof Float) { + this.put(key, (Float) value + 1); + } else { + throw new JSONException("Unable to increment [" + JSONUtil.quote(key) + "]."); + } + return this; + } + + @Override + public Object remove(Object key) { + return rawHashMap.remove(key); + } + + @Override + public void clear() { + rawHashMap.clear(); + } + + @Override + public Set keySet() { + return this.rawHashMap.keySet(); + } + + @Override + public Collection values() { + return rawHashMap.values(); + } + + @Override + public Set> entrySet() { + return rawHashMap.entrySet(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((rawHashMap == null) ? 0 : rawHashMap.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 JSONObject other = (JSONObject) obj; + if (rawHashMap == null) { + if (other.rawHashMap != null) { + return false; + } + } else if (!rawHashMap.equals(other.rawHashMap)) { + return false; + } + return true; + } + + /** + * 返回JSON字符串
+ * 如果解析错误,返回null + * + * @return JSON字符串 + */ + @Override + public String toString() { + try { + return this.toJSONString(0); + } catch (Exception e) { + return null; + } + } + + /** + * 格式化打印JSON,缩进为4个空格 + * + * @return 格式化后的JSON字符串 + * @throws JSONException 包含非法数抛出此异常 + * @since 3.0.9 + */ + @Override + public String toStringPretty() throws JSONException { + return this.toJSONString(4); + } + + /** + * 格式化输出JSON字符串 + * + * @param indentFactor 每层缩进空格数 + * @return JSON字符串 + * @throws JSONException 包含非法数抛出此异常 + */ + @Override + public String toJSONString(int indentFactor) throws JSONException { + final StringWriter w = new StringWriter(); + return this.write(w, indentFactor, 0).toString(); + } + + @Override + public Writer write(Writer writer) throws JSONException { + return this.write(writer, 0, 0); + } + + @Override + public Writer write(Writer writer, int indentFactor, int indent) throws JSONException { + try { + return doWrite(writer, indentFactor, indent); + } catch (IOException exception) { + throw new JSONException(exception); + } + } + + // ------------------------------------------------------------------------------------------------- Private method start + /** + * 将JSON内容写入Writer + * + * @param writer writer + * @param indentFactor 缩进因子,定义每一级别增加的缩进量 + * @param indent 本级别缩进量 + * @return Writer + * @throws JSONException JSON相关异常 + */ + private Writer doWrite(Writer writer, int indentFactor, int indent) throws IOException { + writer.write(CharUtil.DELIM_START); + boolean isFirst = true; + final boolean isIgnoreNullValue = this.config.isIgnoreNullValue(); + final int newIndent = indent + indentFactor; + for (Entry entry : this.entrySet()) { + if (ObjectUtil.isNull(entry.getKey()) || (ObjectUtil.isNull(entry.getValue()) && isIgnoreNullValue)) { + continue; + } + + if (isFirst) { + isFirst = false; + } else { + // 键值对分隔 + writer.write(CharUtil.COMMA); + } + // 换行缩进 + if (indentFactor > 0) { + writer.write(CharUtil.LF); + } + + InternalJSONUtil.indent(writer, newIndent); + writer.write(JSONUtil.quote(entry.getKey().toString())); + writer.write(CharUtil.COLON); + if (indentFactor > 0) { + // 冒号后的空格 + writer.write(CharUtil.SPACE); + } + InternalJSONUtil.writeValue(writer, entry.getValue(), indentFactor, newIndent, this.config); + } + + // 结尾符 + if (indentFactor > 0) { + writer.write(CharUtil.LF); + } + InternalJSONUtil.indent(writer, indent); + writer.write(CharUtil.DELIM_END); + return writer; + } + + /** + * Bean对象转Map + * + * @param bean Bean对象 + * @param ignoreNullValue 是否忽略空值 + */ + private void populateMap(Object bean) { + final Collection props = BeanUtil.getBeanDesc(bean.getClass()).getProps(); + + Method getter; + Object value; + for (PropDesc prop : props) { + // 得到property对应的getter方法 + getter = prop.getGetter(); + if (null == getter) { + // 无Getter跳过 + continue; + } + + // 只读取有getter方法的属性 + try { + value = getter.invoke(bean); + } catch (Exception ignore) { + // 忽略读取失败的属性 + continue; + } + + if (ObjectUtil.isNull(value) && this.config.isIgnoreNullValue()) { + // 值为null且用户定义跳过则跳过 + continue; + } + + if (value != bean) { + // 防止循环引用 + this.rawHashMap.put(prop.getFieldName(), JSONUtil.wrap(value, this.config.isIgnoreNullValue())); + } + } + } + + /** + * 初始化 + *
    + *
  1. value为Map,将键值对加入JSON对象
  2. + *
  3. value为JSON字符串(CharSequence),使用JSONTokener解析
  4. + *
  5. value为JSONTokener,直接解析
  6. + *
  7. value为普通JavaBean,如果为普通的JavaBean,调用其getters方法(getXXX或者isXXX)获得值,加入到JSON对象。例如:如果JavaBean对象中有个方法getName(),值为"张三",获得的键值对为:name: "张三"
  8. + *
+ * + * @param source JavaBean或者Map对象或者String + */ + private void init(Object source) { + if (null == source) { + return; + } + + if (source instanceof Map) { + boolean ignoreNullValue = this.config.isIgnoreNullValue(); + for (final Entry e : ((Map) source).entrySet()) { + final Object value = e.getValue(); + if (false == ignoreNullValue || null != value) { + this.rawHashMap.put(Convert.toStr(e.getKey()), JSONUtil.wrap(value, ignoreNullValue)); + } + } + } else if (source instanceof CharSequence) { + // 可能为JSON字符串 + init((CharSequence) source); + } else if (source instanceof JSONTokener) { + init((JSONTokener) source); + } else if (source instanceof Number) { + // ignore Number + } else { + // 普通Bean + this.populateMap(source); + } + } + + /** + * 初始化 + * + * @param source JSON字符串 + */ + private void init(CharSequence source) { + init(new JSONTokener(StrUtil.trim(source))); + } + + /** + * 初始化 + * + * @param x JSONTokener + */ + private void init(JSONTokener x) { + char c; + String key; + + if (x.nextClean() != '{') { + throw x.syntaxError("A JSONObject text must begin with '{'"); + } + for (;;) { + c = x.nextClean(); + switch (c) { + case 0: + throw x.syntaxError("A JSONObject text must end with '}'"); + case '}': + return; + default: + x.back(); + key = x.nextValue().toString(); + } + + // The key is followed by ':'. + + c = x.nextClean(); + if (c != ':') { + throw x.syntaxError("Expected a ':' after a key"); + } + this.putOnce(key, x.nextValue()); + + // Pairs are separated by ','. + + switch (x.nextClean()) { + case ';': + case ',': + if (x.nextClean() == '}') { + return; + } + x.back(); + break; + case '}': + return; + default: + throw x.syntaxError("Expected a ',' or '}'"); + } + } + } + // ------------------------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-json/src/main/java/cn/hutool/json/JSONObjectIter.java b/hutool-json/src/main/java/cn/hutool/json/JSONObjectIter.java new file mode 100644 index 000000000..660a8569a --- /dev/null +++ b/hutool-json/src/main/java/cn/hutool/json/JSONObjectIter.java @@ -0,0 +1,40 @@ +package cn.hutool.json; + +import java.util.Iterator; + +/** + * 此类用于在JSONAray中便于遍历JSONObject而封装的Iterable,可以借助foreach语法遍历 + * + * @author looly + * @since 4.0.12 + */ +public class JSONObjectIter implements Iterable { + + Iterator iter; + + public JSONObjectIter(Iterator iter) { + this.iter = iter; + } + + @Override + public Iterator iterator() { + return new Iterator() { + + @Override + public boolean hasNext() { + return iter.hasNext(); + } + + @Override + public JSONObject next() { + return (JSONObject) iter.next(); + } + + @Override + public void remove() { + iter.remove(); + } + }; + } + +} diff --git a/hutool-json/src/main/java/cn/hutool/json/JSONStrFormater.java b/hutool-json/src/main/java/cn/hutool/json/JSONStrFormater.java new file mode 100644 index 000000000..ee39de570 --- /dev/null +++ b/hutool-json/src/main/java/cn/hutool/json/JSONStrFormater.java @@ -0,0 +1,123 @@ +package cn.hutool.json; + +import cn.hutool.core.util.StrUtil; + +/** + * JSON字符串格式化工具,用于简单格式化JSON字符串
+ * from http://blog.csdn.net/lovelong8808/article/details/54580278 + * + * @author looly + * @since 3.1.2 + */ +public class JSONStrFormater { + + /** 单位缩进字符串。*/ + private static String SPACE = " "; + /** 换行符*/ + private static char NEW_LINE = StrUtil.C_LF; + + /** + * 返回格式化JSON字符串。 + * + * @param json 未格式化的JSON字符串。 + * @return 格式化的JSON字符串。 + */ + public static String format(String json) { + final StringBuffer result = new StringBuffer(); + + Character wrapChar = null; + boolean isEscapeMode = false; + int length = json.length(); + int number = 0; + char key = 0; + for (int i = 0; i < length; i++) { + key = json.charAt(i); + + if('"' == key || '\'' == key) { + if(null == wrapChar) { + //字符串模式开始 + wrapChar = key; + }else if(isEscapeMode) { + //在字符串模式下的转义 + isEscapeMode = false; + }else if(wrapChar.equals(key)){ + //字符串包装结束 + wrapChar = null; + } + result.append(key); + continue; + }else if('\\' == key) { + if(null != wrapChar) { + //字符串模式下转义有效 + isEscapeMode = !isEscapeMode; + result.append(key); + continue; + }else { + result.append(key); + } + } + + if(null != wrapChar) { + //字符串模式 + result.append(key); + continue; + } + + //如果当前字符是前方括号、前花括号做如下处理: + if ((key == '[') || (key == '{')) { + //如果前面还有字符,并且字符为“:”,打印:换行和缩进字符字符串。 + if ((i - 1 > 0) && (json.charAt(i - 1) == ':')) { + result.append(NEW_LINE); + result.append(indent(number)); + } + result.append(key); + //前方括号、前花括号,的后面必须换行。打印:换行。 + result.append(NEW_LINE); + //每出现一次前方括号、前花括号;缩进次数增加一次。打印:新行缩进。 + number++; + result.append(indent(number)); + + continue; + } + + // 3、如果当前字符是后方括号、后花括号做如下处理: + if ((key == ']') || (key == '}')) { + // (1)后方括号、后花括号,的前面必须换行。打印:换行。 + result.append(NEW_LINE); + // (2)每出现一次后方括号、后花括号;缩进次数减少一次。打印:缩进。 + number--; + result.append(indent(number)); + // (3)打印:当前字符。 + result.append(key); + // (4)如果当前字符后面还有字符,并且字符不为“,”,打印:换行。 + if (((i + 1) < length) && (json.charAt(i + 1) != ',')) { + result.append(NEW_LINE); + } + // (5)继续下一次循环。 + continue; + } + + // 4、如果当前字符是逗号。逗号后面换行,并缩进,不改变缩进次数。 + if ((key == ',')) { + result.append(key); + result.append(NEW_LINE); + result.append(indent(number)); + continue; + } + // 5、打印:当前字符。 + result.append(key); + } + + return result.toString(); + } + + /** + * 返回指定次数的缩进字符串。每一次缩进4个空格,即SPACE。 + * + * @param number 缩进次数。 + * @return 指定缩进次数的字符串。 + */ + private static String indent(int number) { + return StrUtil.repeat(SPACE, number); + } +} diff --git a/hutool-json/src/main/java/cn/hutool/json/JSONString.java b/hutool-json/src/main/java/cn/hutool/json/JSONString.java new file mode 100644 index 000000000..0398b8a3f --- /dev/null +++ b/hutool-json/src/main/java/cn/hutool/json/JSONString.java @@ -0,0 +1,18 @@ +package cn.hutool.json; + +/** + * JSONString接口定义了一个toJSONString()
+ * 实现此接口的类可以通过实现toJSONString()方法来改变转JSON字符串的方式。 + * + * @author Looly + * + */ +public interface JSONString { + + /** + * 自定义转JSON字符串的方法 + * + * @return JSON字符串 + */ + public String toJSONString(); +} diff --git a/hutool-json/src/main/java/cn/hutool/json/JSONSupport.java b/hutool-json/src/main/java/cn/hutool/json/JSONSupport.java new file mode 100644 index 000000000..b9a026b47 --- /dev/null +++ b/hutool-json/src/main/java/cn/hutool/json/JSONSupport.java @@ -0,0 +1,45 @@ +package cn.hutool.json; + +/** + * JSON支持
+ * 继承此类实现实体类与JSON的相互转换 + * + * @author Looly + * + */ +public class JSONSupport implements JSONString{ + + /** + * JSON String转Bean + * @param jsonString JSON String + */ + public void parse(String jsonString){ + new JSONObject(jsonString).toBean(this.getClass()); + } + + /** + * @return JSON对象 + */ + public JSONObject toJSON() { + return new JSONObject(this); + } + + @Override + public String toJSONString() { + return toJSON().toString(); + } + + /** + * 美化的JSON(使用回车缩进显示JSON),用于打印输出debug + * + * @return 美化的JSON + */ + public String toPrettyString() { + return toJSON().toJSONString(4); + } + + @Override + public String toString() { + return toJSONString(); + } +} diff --git a/hutool-json/src/main/java/cn/hutool/json/JSONTokener.java b/hutool-json/src/main/java/cn/hutool/json/JSONTokener.java new file mode 100644 index 000000000..cb2e353fe --- /dev/null +++ b/hutool-json/src/main/java/cn/hutool/json/JSONTokener.java @@ -0,0 +1,422 @@ +package cn.hutool.json; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; + +/** + * JSON解析器,用于将JSON字符串解析为JSONObject或者JSONArray + * + * @author from JSON.org + */ +public class JSONTokener { + + private long character; + /** 是否结尾 End of stream */ + private boolean eof; + /** 在Reader的位置(解析到第几个字符) */ + private long index; + /** 当前所在行 */ + private long line; + /** 前一个字符 */ + private char previous; + /** 是否使用前一个字符 */ + private boolean usePrevious; + /** 源 */ + private Reader reader; + + // ------------------------------------------------------------------------------------ Constructor start + /** + * 从Reader中构建 + * + * @param reader Reader + */ + public JSONTokener(Reader reader) { + this.reader = reader.markSupported() ? reader : new BufferedReader(reader); + this.eof = false; + this.usePrevious = false; + this.previous = 0; + this.index = 0; + this.character = 1; + this.line = 1; + } + + /** + * 从InputStream中构建 + * + * @param inputStream InputStream + */ + public JSONTokener(InputStream inputStream) throws JSONException { + this(new InputStreamReader(inputStream)); + } + + /** + * 从字符串中构建 + * + * @param s JSON字符串 + */ + public JSONTokener(String s) { + this(new StringReader(s)); + } + // ------------------------------------------------------------------------------------ Constructor end + + /** + * 将标记回退到第一个字符,重新开始解析新的JSON + */ + public void back() throws JSONException { + if (this.usePrevious || this.index <= 0) { + throw new JSONException("Stepping back two steps is not supported"); + } + this.index -= 1; + this.character -= 1; + this.usePrevious = true; + this.eof = false; + } + + /** + * @return 是否进入结尾 + */ + public boolean end() { + return this.eof && false == this.usePrevious; + } + + /** + * 源字符串是否有更多的字符 + * + * @return 如果未达到结尾返回true,否则false + */ + public boolean more() throws JSONException { + this.next(); + if (this.end()) { + return false; + } + this.back(); + return true; + } + + /** + * 获得源字符串中的下一个字符 + * + * @return 下一个字符, or 0 if past the end of the source string. + * @throws JSONException JSON异常,包装IO异常 + */ + public char next() throws JSONException { + int c; + if (this.usePrevious) { + this.usePrevious = false; + c = this.previous; + } else { + try { + c = this.reader.read(); + } catch (IOException exception) { + throw new JSONException(exception); + } + + if (c <= 0) { // End of stream + this.eof = true; + c = 0; + } + } + this.index += 1; + if (this.previous == '\r') { + this.line += 1; + this.character = c == '\n' ? 0 : 1; + } else if (c == '\n') { + this.line += 1; + this.character = 0; + } else { + this.character += 1; + } + this.previous = (char) c; + return this.previous; + } + + /** + * 读取下一个字符,并比对是否和指定字符匹配 + * + * @param c 被匹配的字符 + * @return The character 匹配到的字符 + * @throws JSONException 如果不匹配抛出此异常 + */ + public char next(char c) throws JSONException { + char n = this.next(); + if (n != c) { + throw this.syntaxError("Expected '" + c + "' and instead saw '" + n + "'"); + } + return n; + } + + /** + * 获得接下来的n个字符 + * + * @param n 字符数 + * @return 获得的n个字符组成的字符串 + * @throws JSONException 如果源中余下的字符数不足以提供所需的字符数,抛出此异常 + */ + public String next(int n) throws JSONException { + if (n == 0) { + return ""; + } + + char[] chars = new char[n]; + int pos = 0; + while (pos < n) { + chars[pos] = this.next(); + if (this.end()) { + throw this.syntaxError("Substring bounds error"); + } + pos += 1; + } + return new String(chars); + } + + /** + * 获得下一个字符,跳过空白符 + * + * @throws JSONException 获得下一个字符时抛出的异常 + * @return 获得的字符,0表示没有更多的字符 + */ + public char nextClean() throws JSONException { + char c; + while (true) { + c = this.next(); + if (c == 0 || c > ' ') { + return c; + } + } + } + + /** + * 返回当前位置到指定引号前的所有字符,反斜杠的转义符也会被处理。
+ * 标准的JSON是不允许使用单引号包含字符串的,但是此实现允许。 + * + * @param quote 字符引号, 包括 "(双引号) 或 '(单引号)。 + * @return 截止到引号前的字符串 + * @throws JSONException 出现无结束的字符串时抛出此异常 + */ + public String nextString(char quote) throws JSONException { + char c; + StringBuilder sb = new StringBuilder(); + for (;;) { + c = this.next(); + switch (c) { + case 0: + case '\n': + case '\r': + throw this.syntaxError("Unterminated string"); + case '\\':// 转义符 + c = this.next(); + switch (c) { + case 'b': + sb.append('\b'); + break; + case 't': + sb.append('\t'); + break; + case 'n': + sb.append('\n'); + break; + case 'f': + sb.append('\f'); + break; + case 'r': + sb.append('\r'); + break; + case 'u':// Unicode符 + sb.append((char) Integer.parseInt(this.next(4), 16)); + break; + case '"': + case '\'': + case '\\': + case '/': + sb.append(c); + break; + default: + throw this.syntaxError("Illegal escape."); + } + break; + default: + if (c == quote) { + return sb.toString(); + } + sb.append(c); + } + } + } + + /** + * Get the text up but not including the specified character or the end of line, whichever comes first.
+ * 获得从当前位置直到分隔符(不包括分隔符)或行尾的的所有字符。 + * + * @param delimiter 分隔符 + * @return 字符串 + */ + public String nextTo(char delimiter) throws JSONException { + StringBuilder sb = new StringBuilder(); + for (;;) { + char c = this.next(); + if (c == delimiter || c == 0 || c == '\n' || c == '\r') { + if (c != 0) { + this.back(); + } + return sb.toString().trim(); + } + sb.append(c); + } + } + + /** + * Get the text up but not including one of the specified delimiter characters or the end of line, whichever comes first. + * + * @param delimiters A set of delimiter characters. + * @return A string, trimmed. + */ + public String nextTo(String delimiters) throws JSONException { + char c; + StringBuilder sb = new StringBuilder(); + for (;;) { + c = this.next(); + if (delimiters.indexOf(c) >= 0 || c == 0 || c == '\n' || c == '\r') { + if (c != 0) { + this.back(); + } + return sb.toString().trim(); + } + sb.append(c); + } + } + + /** + * 获得下一个值,值类型可以是Boolean, Double, Integer, JSONArray, JSONObject, Long, or String, or the JSONObject.NULL + * + * @throws JSONException 语法错误 + * + * @return Boolean, Double, Integer, JSONArray, JSONObject, Long, or String, or the JSONObject.NULL + */ + public Object nextValue() throws JSONException { + char c = this.nextClean(); + String string; + + switch (c) { + case '"': + case '\'': + return this.nextString(c); + case '{': + this.back(); + return new JSONObject(this); + case '[': + this.back(); + return new JSONArray(this); + } + + /* + * Handle unquoted text. This could be the values true, false, or null, or it can be a number. + * An implementation (such as this one) is allowed to also accept non-standard forms. Accumulate + * characters until we reach the end of the text or a formatting character. + */ + + StringBuilder sb = new StringBuilder(); + while (c >= ' ' && ",:]}/\\\"[{;=#".indexOf(c) < 0) { + sb.append(c); + c = this.next(); + } + this.back(); + + string = sb.toString().trim(); + if ("".equals(string)) { + throw this.syntaxError("Missing value"); + } + return InternalJSONUtil.stringToValue(string); + } + + /** + * Skip characters until the next character is the requested character. If the requested character is not found, no characters are skipped. 在遇到指定字符前,跳过其它字符。如果字符未找到,则不跳过任何字符。 + * + * @param to 需要定位的字符 + * @return 定位的字符,如果字符未找到返回0 + */ + public char skipTo(char to) throws JSONException { + char c; + try { + long startIndex = this.index; + long startCharacter = this.character; + long startLine = this.line; + this.reader.mark(1000000); + do { + c = this.next(); + if (c == 0) { + this.reader.reset(); + this.index = startIndex; + this.character = startCharacter; + this.line = startLine; + return c; + } + } while (c != to); + } catch (IOException exception) { + throw new JSONException(exception); + } + this.back(); + return c; + } + + /** + * Make a JSONException to signal a syntax error.
+ * 构建 JSONException 用于表示语法错误 + * + * @param message 错误消息 + * @return A JSONException object, suitable for throwing + */ + public JSONException syntaxError(String message) { + return new JSONException(message + this.toString()); + } + + /** + * 转为 {@link JSONArray} + * + * @return {@link JSONArray} + */ + public JSONArray toJSONArray() { + JSONArray jsonArray = new JSONArray(); + if (this.nextClean() != '[') { + throw this.syntaxError("A JSONArray text must start with '['"); + } + if (this.nextClean() != ']') { + this.back(); + while (true) { + if (this.nextClean() == ',') { + this.back(); + jsonArray.add(JSONNull.NULL); + } else { + this.back(); + jsonArray.add(this.nextValue()); + } + switch (this.nextClean()) { + case ',': + if (this.nextClean() == ']') { + return jsonArray; + } + this.back(); + break; + case ']': + return jsonArray; + default: + throw this.syntaxError("Expected a ',' or ']'"); + } + } + } + return jsonArray; + } + + /** + * Make a printable string of this JSONTokener. + * + * @return " at {index} [character {character} line {line}]" + */ + @Override + public String toString() { + return " at " + this.index + " [character " + this.character + " line " + this.line + "]"; + } +} diff --git a/hutool-json/src/main/java/cn/hutool/json/JSONUtil.java b/hutool-json/src/main/java/cn/hutool/json/JSONUtil.java new file mode 100644 index 000000000..d908c4bcd --- /dev/null +++ b/hutool-json/src/main/java/cn/hutool/json/JSONUtil.java @@ -0,0 +1,737 @@ +package cn.hutool.json; + +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.reflect.Type; +import java.nio.charset.Charset; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.file.FileReader; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.HexUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; + +/** + * JSON工具类 + * + * @author Looly + * + */ +public final class JSONUtil { + + // -------------------------------------------------------------------- Pause start + /** + * 创建JSONObject + * + * @return JSONObject + */ + public static JSONObject createObj() { + return new JSONObject(); + } + + /** + * 创建 JSONArray + * + * @return JSONArray + */ + public static JSONArray createArray() { + return new JSONArray(); + } + + /** + * JSON字符串转JSONObject对象 + * + * @param jsonStr JSON字符串 + * @return JSONObject + */ + public static JSONObject parseObj(String jsonStr) { + return new JSONObject(jsonStr); + } + + /** + * JSON字符串转JSONObject对象
+ * 此方法会忽略空值,但是对JSON字符串不影响 + * + * @param obj Bean对象或者Map + * @return JSONObject + */ + public static JSONObject parseObj(Object obj) { + return new JSONObject(obj); + } + + /** + * JSON字符串转JSONObject对象 + * + * @param obj Bean对象或者Map + * @param ignoreNullValue 是否忽略空值,如果source为JSON字符串,不忽略空值 + * @return JSONObject + * @since 3.0.9 + */ + public static JSONObject parseObj(Object obj, boolean ignoreNullValue) { + return new JSONObject(obj, ignoreNullValue); + } + + /** + * JSON字符串转JSONObject对象 + * + * @param obj Bean对象或者Map + * @param ignoreNullValue 是否忽略空值,如果source为JSON字符串,不忽略空值 + * @param isOrder 是否有序 + * @return JSONObject + * @since 4.2.2 + */ + public static JSONObject parseObj(Object obj, boolean ignoreNullValue, boolean isOrder) { + return new JSONObject(obj, ignoreNullValue, isOrder); + } + + /** + * JSON字符串转JSONArray + * + * @param jsonStr JSON字符串 + * @return JSONArray + */ + public static JSONArray parseArray(String jsonStr) { + return new JSONArray(jsonStr); + } + + /** + * JSON字符串转JSONArray + * + * @param arrayOrCollection 数组或集合对象 + * @return JSONArray + * @since 3.0.8 + */ + public static JSONArray parseArray(Object arrayOrCollection) { + return new JSONArray(arrayOrCollection); + } + + /** + * JSON字符串转JSONArray + * + * @param arrayOrCollection 数组或集合对象 + * @param ignoreNullValue 是否忽略空值 + * @return JSONArray + * @since 3.2.3 + */ + public static JSONArray parseArray(Object arrayOrCollection, boolean ignoreNullValue) { + return new JSONArray(arrayOrCollection, ignoreNullValue); + } + + /** + * 转换对象为JSON
+ * 支持的对象:
+ * String: 转换为相应的对象
+ * Array Collection:转换为JSONArray
+ * Bean对象:转为JSONObject + * + * @param obj 对象 + * @return JSON + */ + public static JSON parse(Object obj) { + if (null == obj) { + return null; + } + + JSON json = null; + if (obj instanceof JSON) { + json = (JSON) obj; + } else if (obj instanceof String) { + String jsonStr = ((String) obj).trim(); + if (jsonStr.startsWith("[")) { + json = parseArray(jsonStr); + } else { + json = parseObj(jsonStr); + } + } else if (obj instanceof Collection || obj.getClass().isArray()) {// 列表 + json = new JSONArray(obj); + } else {// 对象 + json = new JSONObject(obj); + } + + return json; + } + + /** + * XML字符串转为JSONObject + * + * @param xmlStr XML字符串 + * @return JSONObject + */ + public static JSONObject parseFromXml(String xmlStr) { + return XML.toJSONObject(xmlStr); + } + + /** + * Map转化为JSONObject + * + * @param map {@link Map} + * @return JSONObject + */ + public static JSONObject parseFromMap(Map map) { + return new JSONObject(map); + } + + /** + * ResourceBundle转化为JSONObject + * + * @param bundle ResourceBundle文件 + * @return JSONObject + */ + public static JSONObject parseFromResourceBundle(ResourceBundle bundle) { + JSONObject jsonObject = new JSONObject(); + Enumeration keys = bundle.getKeys(); + while (keys.hasMoreElements()) { + String key = keys.nextElement(); + if (key != null) { + InternalJSONUtil.propertyPut(jsonObject, key, bundle.getString(key)); + } + } + return jsonObject; + } + // -------------------------------------------------------------------- Pause end + + // -------------------------------------------------------------------- Read start + /** + * 读取JSON + * + * @param file JSON文件 + * @param charset 编码 + * @return JSON(包括JSONObject和JSONArray) + * @throws IORuntimeException IO异常 + */ + public static JSON readJSON(File file, Charset charset) throws IORuntimeException { + return parse(FileReader.create(file, charset).readString()); + } + + /** + * 读取JSONObject + * + * @param file JSON文件 + * @param charset 编码 + * @return JSONObject + * @throws IORuntimeException IO异常 + */ + public static JSONObject readJSONObject(File file, Charset charset) throws IORuntimeException { + return parseObj(FileReader.create(file, charset).readString()); + } + + /** + * 读取JSONArray + * + * @param file JSON文件 + * @param charset 编码 + * @return JSONArray + * @throws IORuntimeException IO异常 + */ + public static JSONArray readJSONArray(File file, Charset charset) throws IORuntimeException { + return parseArray(FileReader.create(file, charset).readString()); + } + // -------------------------------------------------------------------- Read end + + // -------------------------------------------------------------------- toString start + /** + * 转为JSON字符串 + * + * @param json JSON + * @param indentFactor 每一级别的缩进 + * @return JSON字符串 + */ + public static String toJsonStr(JSON json, int indentFactor) { + if (null == json) { + return null; + } + return json.toJSONString(indentFactor); + } + + /** + * 转为JSON字符串 + * + * @param json JSON + * @return JSON字符串 + */ + public static String toJsonStr(JSON json) { + if (null == json) { + return null; + } + return json.toJSONString(0); + } + + /** + * 转为JSON字符串 + * + * @param json JSON + * @return JSON字符串 + */ + public static String toJsonPrettyStr(JSON json) { + if (null == json) { + return null; + } + return json.toJSONString(4); + } + + /** + * 转换为JSON字符串 + * + * @param obj 被转为JSON的对象 + * @return JSON字符串 + */ + public static String toJsonStr(Object obj) { + if (null == obj) { + return null; + } + if (obj instanceof String) { + return (String) obj; + } + return toJsonStr(parse(obj)); + } + + /** + * 转换为格式化后的JSON字符串 + * + * @param obj Bean对象 + * @return JSON字符串 + */ + public static String toJsonPrettyStr(Object obj) { + return toJsonPrettyStr(parse(obj)); + } + + /** + * 转换为XML字符串 + * + * @param json JSON + * @return XML字符串 + */ + public static String toXmlStr(JSON json) { + return XML.toXml(json); + } + // -------------------------------------------------------------------- toString end + + // -------------------------------------------------------------------- toBean start + + /** + * JSON字符串转为实体类对象,转换异常将被抛出 + * + * @param Bean类型 + * @param jsonString JSON字符串 + * @param beanClass 实体类对象 + * @return 实体类对象 + * @since 3.1.2 + */ + public static T toBean(String jsonString, Class beanClass) { + return toBean(parseObj(jsonString), beanClass); + } + + /** + * 转为实体类对象,转换异常将被抛出 + * + * @param Bean类型 + * @param json JSONObject + * @param beanClass 实体类对象 + * @return 实体类对象 + */ + public static T toBean(JSONObject json, Class beanClass) { + return null == json ? null : json.toBean(beanClass); + } + + /** + * JSON字符串转为实体类对象,转换异常将被抛出 + * + * @param Bean类型 + * @param jsonString JSON字符串 + * @param beanType 实体类对象类型 + * @return 实体类对象 + * @since 4.3.2 + */ + public static T toBean(String jsonString, Type beanType, boolean ignoreError) { + return toBean(parseObj(jsonString), beanType, ignoreError); + } + + /** + * 转为实体类对象 + * + * @param Bean类型 + * @param json JSONObject + * @param beanType 实体类对象类型 + * @param ignoreError 是否忽略转换错误 + * @return 实体类对象 + * @since 4.3.2 + */ + public static T toBean(JSONObject json, Type beanType, boolean ignoreError) { + if (null == json) { + return null; + } + return json.toBean(beanType, ignoreError); + } + // -------------------------------------------------------------------- toBean end + + /** + * 将JSONArray转换为Bean的List,默认为ArrayList + * + * @param jsonArray JSONArray + * @param elementType List中元素类型 + * @return List + * @since 4.0.7 + */ + public static List toList(JSONArray jsonArray, Class elementType) { + return null == jsonArray ? null : jsonArray.toList(elementType); + } + + /** + * 通过表达式获取JSON中嵌套的对象
+ *
    + *
  1. .表达式,可以获取Bean对象中的属性(字段)值或者Map中key对应的值
  2. + *
  3. []表达式,可以获取集合等对象中对应index的值
  4. + *
+ * + * 表达式栗子: + * + *
+	 * persion
+	 * persion.name
+	 * persons[3]
+	 * person.friends[5].name
+	 * 
+ * + * @param json {@link JSON} + * @param expression 表达式 + * @return 对象 + * @see JSON#getByPath(String) + */ + public static Object getByPath(JSON json, String expression) { + return (null == json || StrUtil.isBlank(expression)) ? null : json.getByPath(expression); + } + + /** + * 设置表达式指定位置(或filed对应)的值
+ * 若表达式指向一个JSONArray则设置其坐标对应位置的值,若指向JSONObject则put对应key的值
+ * 注意:如果为JSONArray,则设置值得下标不能大于已有JSONArray的长度
+ *
    + *
  1. .表达式,可以获取Bean对象中的属性(字段)值或者Map中key对应的值
  2. + *
  3. []表达式,可以获取集合等对象中对应index的值
  4. + *
+ * + * 表达式栗子: + * + *
+	 * persion
+	 * persion.name
+	 * persons[3]
+	 * person.friends[5].name
+	 * 
+ * + * @param json JSON,可以为JSONObject或JSONArray + * @param expression 表达式 + * @param value 值 + */ + public static void putByPath(JSON json, String expression, Object value) { + json.putByPath(expression, value); + } + + /** + * 对所有双引号做转义处理(使用双反斜杠做转义)
+ * 为了能在HTML中较好的显示,会将</转义为<\/
+ * JSON字符串中不能包含控制字符和未经转义的引号和反斜杠 + * + * @param string 字符串 + * @return 适合在JSON中显示的字符串 + */ + public static String quote(String string) { + return quote(string, true); + } + + /** + * 对所有双引号做转义处理(使用双反斜杠做转义)
+ * 为了能在HTML中较好的显示,会将</转义为<\/
+ * JSON字符串中不能包含控制字符和未经转义的引号和反斜杠 + * + * @param string 字符串 + * @param isWrap 是否使用双引号包装字符串 + * @return 适合在JSON中显示的字符串 + * @since 3.3.1 + */ + public static String quote(String string, boolean isWrap) { + StringWriter sw = new StringWriter(); + try { + return quote(string, sw, isWrap).toString(); + } catch (IOException ignored) { + // will never happen - we are writing to a string writer + return StrUtil.EMPTY; + } + } + + /** + * 对所有双引号做转义处理(使用双反斜杠做转义)
+ * 为了能在HTML中较好的显示,会将</转义为<\/
+ * JSON字符串中不能包含控制字符和未经转义的引号和反斜杠 + * + * @param str 字符串 + * @param writer Writer + * @return Writer + * @throws IOException IO异常 + */ + public static Writer quote(String str, Writer writer) throws IOException { + return quote(str, writer, true); + } + + /** + * 对所有双引号做转义处理(使用双反斜杠做转义)
+ * 为了能在HTML中较好的显示,会将</转义为<\/
+ * JSON字符串中不能包含控制字符和未经转义的引号和反斜杠 + * + * @param str 字符串 + * @param writer Writer + * @param isWrap 是否使用双引号包装字符串 + * @return Writer + * @throws IOException IO异常 + * @since 3.3.1 + */ + public static Writer quote(String str, Writer writer, boolean isWrap) throws IOException { + if (StrUtil.isEmpty(str)) { + if (isWrap) { + writer.write("\"\""); + } + return writer; + } + + char b; // 前一个字符 + char c = 0; // 当前字符 + int len = str.length(); + if (isWrap) { + writer.write('"'); + } + for (int i = 0; i < len; i++) { + b = c; + c = str.charAt(i); + switch (c) { + case '\\': + case '"': + writer.write("\\"); + writer.write(c); + break; + case '/': + if (b == '<') { + writer.write('\\'); + } + writer.write(c); + break; + default: + writer.write(escape(c)); + } + } + if (isWrap) { + writer.write('"'); + } + return writer; + } + + /** + * 转义显示不可见字符 + * + * @param str 字符串 + * @return 转义后的字符串 + */ + public static String escape(String str) { + if (StrUtil.isEmpty(str)) { + return str; + } + + final int len = str.length(); + final StringBuilder builder = new StringBuilder(len); + char c; + for (int i = 0; i < len; i++) { + c = str.charAt(i); + builder.append(escape(c)); + } + return builder.toString(); + } + + /** + * 在需要的时候包装对象
+ * 包装包括: + *
    + *
  • null =》 JSONNull.NULL
  • + *
  • array or collection =》 JSONArray
  • + *
  • map =》 JSONObject
  • + *
  • standard property (Double, String, et al) =》 原对象
  • + *
  • 来自于java包 =》 字符串
  • + *
  • 其它 =》 尝试包装为JSONObject,否则返回null
  • + *
+ * + * @param object 被包装的对象 + * @param ignoreNullValue 是否忽略{@code null} 值 + * @return 包装后的值,null表示此值需被忽略 + */ + public static Object wrap(Object object, boolean ignoreNullValue) { + if (object == null) { + return ignoreNullValue ? null : JSONNull.NULL; + } + if (object instanceof JSON // + || JSONNull.NULL.equals(object) // + || object instanceof JSONString // + || object instanceof CharSequence // + || object instanceof Number // + || ObjectUtil.isBasicType(object) // + ) { + return object; + } + + try { + // JSONArray + if (object instanceof Iterable || ArrayUtil.isArray(object)) { + return new JSONArray(object, ignoreNullValue); + } + // JSONObject + if (object instanceof Map) { + return new JSONObject(object, ignoreNullValue); + } + + // 日期类型原样保存,便于格式化 + if (object instanceof Date || object instanceof Calendar) { + return object; + } + if (object instanceof Calendar) { + return ((Calendar) object).getTimeInMillis(); + } + // 枚举类保存其字符串形式(4.0.2新增) + if (object instanceof Enum) { + return object.toString(); + } + + // Java内部类不做转换 + final Class objectClass = object.getClass(); + final Package objectPackage = objectClass.getPackage(); + final String objectPackageName = objectPackage != null ? objectPackage.getName() : ""; + if (objectPackageName.startsWith("java.") || objectPackageName.startsWith("javax.") || objectClass.getClassLoader() == null) { + return object.toString(); + } + + // 默认按照JSONObject对待 + return new JSONObject(object, ignoreNullValue); + } catch (Exception exception) { + return null; + } + } + + /** + * 格式化JSON字符串,此方法并不严格检查JSON的格式正确与否 + * + * @param jsonStr JSON字符串 + * @return 格式化后的字符串 + * @since 3.1.2 + */ + public static String formatJsonStr(String jsonStr) { + return JSONStrFormater.format(jsonStr); + } + + /** + * 是否为JSON字符串,首尾都为大括号或中括号判定为JSON字符串 + * + * @param str 字符串 + * @return 是否为JSON字符串 + * @since 3.3.0 + */ + public static boolean isJson(String str) { + return isJsonObj(str) || isJsonArray(str); + } + + /** + * 是否为JSONObject字符串,首尾都为大括号或中括号判定为JSON字符串 + * + * @param str 字符串 + * @return 是否为JSON字符串 + * @since 3.3.0 + */ + public static boolean isJsonObj(String str) { + if (StrUtil.isBlank(str)) { + return false; + } + return StrUtil.isWrap(str.trim(), '{', '}'); + } + + /** + * 是否为JSONObject字符串,首尾都为大括号或中括号判定为JSON字符串 + * + * @param str 字符串 + * @return 是否为JSON字符串 + * @since 3.3.0 + */ + public static boolean isJsonArray(String str) { + if (StrUtil.isBlank(str)) { + return false; + } + return StrUtil.isWrap(str.trim(), '[', ']'); + } + + /** + * 是否为null对象,null的情况包括: + * + *
+	 * 1. {@code null}
+	 * 2. {@link JSONNull}
+	 * 
+ * + * @param obj 对象 + * @return 是否为null + * @since 4.5.7 + */ + public static boolean isNull(Object obj) { + return null == obj || obj instanceof JSONNull; + } + + /** + * XML转JSONObject
+ * 转换过程中一些信息可能会丢失,JSON中无法区分节点和属性,相同的节点将被处理为JSONArray。 + * + * @param xml XML字符串 + * @return JSONObject + * @since 4.0.8 + */ + public static JSONObject xmlToJson(String xml) { + return XML.toJSONObject(xml); + } + + // --------------------------------------------------------------------------------------------- Private method start + /** + * 转义不可见字符
+ * 见:https://en.wikibooks.org/wiki/Unicode/Character_reference/0000-0FFF + * + * @param c 字符 + * @return 转义后的字符串 + */ + private static String escape(char c) { + switch (c) { + case '\b': + return "\\b"; + case '\t': + return "\\t"; + case '\n': + return "\\n"; + case '\f': + return "\\f"; + case '\r': + return "\\r"; + default: + if (c < StrUtil.C_SPACE || // + (c >= '\u0080' && c <= '\u00a0') || // + (c >= '\u2000' && c <= '\u2010') || // + (c >= '\u2028' && c <= '\u202F') || // + (c >= '\u2066' && c <= '\u206F')// + ) { + return HexUtil.toUnicodeHex(c); + } else { + return Character.toString(c); + } + } + } + // --------------------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-json/src/main/java/cn/hutool/json/XML.java b/hutool-json/src/main/java/cn/hutool/json/XML.java new file mode 100644 index 000000000..e600a6a82 --- /dev/null +++ b/hutool-json/src/main/java/cn/hutool/json/XML.java @@ -0,0 +1,369 @@ +package cn.hutool.json; + +import java.util.Iterator; + +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.XmlUtil; + +/** + * 提供静态方法在XML和JSONObject之间转换 + * + * @author JSON.org + */ +public class XML { + + /** The Character '&'. */ + public static final Character AMP = CharUtil.AMP; + + /** The Character '''. */ + public static final Character APOS = CharUtil.SINGLE_QUOTE; + + /** The Character '!'. */ + public static final Character BANG = '!'; + + /** The Character '='. */ + public static final Character EQ = '='; + + /** The Character '>'. */ + public static final Character GT = '>'; + + /** The Character '<'. */ + public static final Character LT = '<'; + + /** The Character '?'. */ + public static final Character QUEST = '?'; + + /** The Character '"'. */ + public static final Character QUOT = CharUtil.DOUBLE_QUOTES; + + /** The Character '/'. */ + public static final Character SLASH = CharUtil.SLASH; + + /** + * Scan the content following the named tag, attaching it to the context. + * + * @param x The XMLTokener containing the source string. + * @param context The JSONObject that will include the new material. + * @param name The tag name. + * @return true if the close tag is processed. + * @throws JSONException + */ + private static boolean parse(XMLTokener x, JSONObject context, String name, boolean keepStrings) throws JSONException { + char c; + int i; + JSONObject jsonobject = null; + String string; + String tagName; + Object token; + + // Test for and skip past these forms: + // + // + // + // + // Report errors for these forms: + // <> + // <= + // << + + token = x.nextToken(); + + // "); + return false; + } + x.back(); + } else if (c == '[') { + token = x.nextToken(); + if ("CDATA".equals(token)) { + if (x.next() == '[') { + string = x.nextCDATA(); + if (string.length() > 0) { + context.accumulate("content", string); + } + return false; + } + } + throw x.syntaxError("Expected 'CDATA['"); + } + i = 1; + do { + token = x.nextMeta(); + if (token == null) { + throw x.syntaxError("Missing '>' after ' 0); + return false; + } else if (token == QUEST) { + + // "); + return false; + } else if (token == SLASH) { + + // Close tag + if (x.nextToken() != GT) { + throw x.syntaxError("Misshaped tag"); + } + if (jsonobject.size() > 0) { + context.accumulate(tagName, jsonobject); + } else { + context.accumulate(tagName, ""); + } + return false; + + } else if (token == GT) { + // Content, between <...> and + for (;;) { + token = x.nextContent(); + if (token == null) { + if (tagName != null) { + throw x.syntaxError("Unclosed tag " + tagName); + } + return false; + } else if (token instanceof String) { + string = (String) token; + if (string.length() > 0) { + jsonobject.accumulate("content", keepStrings ? token : InternalJSONUtil.stringToValue(string)); + } + + } else if (token == LT) { + // Nested element + if (parse(x, jsonobject, tagName, keepStrings)) { + if (jsonobject.size() == 0) { + context.accumulate(tagName, ""); + } else if (jsonobject.size() == 1 && jsonobject.get("content") != null) { + context.accumulate(tagName, jsonobject.get("content")); + } else { + context.accumulate(tagName, jsonobject); + } + return false; + } + } + } + } else { + throw x.syntaxError("Misshaped tag"); + } + } + } + } + + /** + * 转换XML为JSONObject + * 转换过程中一些信息可能会丢失,JSON中无法区分节点和属性,相同的节点将被处理为JSONArray。 + * Content text may be placed in a "content" member. Comments, prologs, DTDs, and <[ [ ]]> are ignored. + * + * @param string The source string. + * @return A JSONObject containing the structured data from the XML string. + * @throws JSONException Thrown if there is an errors while parsing the string + */ + public static JSONObject toJSONObject(String string) throws JSONException { + return toJSONObject(string, false); + } + + /** + * 转换XML为JSONObject + * 转换过程中一些信息可能会丢失,JSON中无法区分节点和属性,相同的节点将被处理为JSONArray。 + * Content text may be placed in a "content" member. Comments, prologs, DTDs, and <[ [ ]]> are ignored. + * All values are converted as strings, for 1, 01, 29.0 will not be coerced to numbers but will instead be the exact value as seen in the XML document. + * + * @param string The source string. + * @param keepStrings If true, then values will not be coerced into boolean or numeric values and will instead be left as strings + * @return A JSONObject containing the structured data from the XML string. + * @throws JSONException Thrown if there is an errors while parsing the string + */ + public static JSONObject toJSONObject(String string, boolean keepStrings) throws JSONException { + JSONObject jo = new JSONObject(); + XMLTokener x = new XMLTokener(string); + while (x.more() && x.skipPast("<")) { + parse(x, jo, null, keepStrings); + } + return jo; + } + + /** + * 转换JSONObject为XML + * Convert a JSONObject into a well-formed, element-normal XML string. + * + * @param object A JSONObject. + * @return A string. + * @throws JSONException Thrown if there is an error parsing the string + */ + public static String toXml(Object object) throws JSONException { + return toXml(object, null); + } + + /** + * 转换JSONObject为XML + * Convert a JSONObject into a well-formed, element-normal XML string. + * + * @param object A JSONObject. + * @param tagName The optional name of the enclosing tag. + * @return A string. + * @throws JSONException Thrown if there is an error parsing the string + */ + public static String toXml(Object object, String tagName) throws JSONException { + if(null == object) { + return null; + } + + StringBuilder sb = new StringBuilder(); + JSONArray ja; + JSONObject jo; + String key; + Iterator keys; + String string; + Object value; + + if (object instanceof JSONObject) { + + // Emit + if (tagName != null) { + sb.append('<'); + sb.append(tagName); + sb.append('>'); + } + + // Loop thru the keys. + jo = (JSONObject) object; + keys = jo.keySet().iterator(); + while (keys.hasNext()) { + key = keys.next(); + value = jo.get(key); + if (value == null) { + value = ""; + } else if (value.getClass().isArray()) { + value = new JSONArray(value); + } + string = value instanceof String ? (String) value : null; + + // Emit content in body + if ("content".equals(key)) { + if (value instanceof JSONArray) { + ja = (JSONArray) value; + int i = 0; + for (Object val : ja) { + if (i > 0) { + sb.append('\n'); + } + sb.append(XmlUtil.escape(val.toString())); + i++; + } + } else { + sb.append(XmlUtil.escape(value.toString())); + } + + // Emit an array of similar keys + + } else if (value instanceof JSONArray) { + ja = (JSONArray) value; + for (Object val : ja) { + if (val instanceof JSONArray) { + sb.append('<'); + sb.append(key); + sb.append('>'); + sb.append(toXml(val)); + sb.append("'); + } else { + sb.append(toXml(val, key)); + } + } + } else if ("".equals(value)) { + sb.append('<'); + sb.append(key); + sb.append("/>"); + + // Emit a new tag + + } else { + sb.append(toXml(value, key)); + } + } + if (tagName != null) { + + // Emit the close tag + sb.append("'); + } + return sb.toString(); + + } + + if (object != null) { + if (object.getClass().isArray()) { + object = new JSONArray(object); + } + + if (object instanceof JSONArray) { + ja = (JSONArray) object; + for (Object val : ja) { + // XML does not have good support for arrays. If an array + // appears in a place where XML is lacking, synthesize an + // element. + sb.append(toXml(val, tagName == null ? "array" : tagName)); + } + return sb.toString(); + } + } + + string = (object == null) ? "null" : XmlUtil.escape(object.toString()); + return (tagName == null) ? "\"" + string + "\"" : (string.length() == 0) ? "<" + tagName + "/>" : "<" + tagName + ">" + string + ""; + + } +} diff --git a/hutool-json/src/main/java/cn/hutool/json/XMLTokener.java b/hutool-json/src/main/java/cn/hutool/json/XMLTokener.java new file mode 100644 index 000000000..79e79b29d --- /dev/null +++ b/hutool-json/src/main/java/cn/hutool/json/XMLTokener.java @@ -0,0 +1,328 @@ +package cn.hutool.json; + +/** + * XML分析器,继承自JSONTokener,提供XML的语法分析 + * + * @author JSON.org + */ +public class XMLTokener extends JSONTokener { + + /** + * The table of entity values. It initially contains Character values for amp, apos, gt, lt, quot. + */ + public static final java.util.HashMap entity; + + static { + entity = new java.util.HashMap(8); + entity.put("amp", XML.AMP); + entity.put("apos", XML.APOS); + entity.put("gt", XML.GT); + entity.put("lt", XML.LT); + entity.put("quot", XML.QUOT); + } + + /** + * Construct an XMLTokener from a string. + * + * @param s A source string. + */ + public XMLTokener(String s) { + super(s); + } + + /** + * Get the text in the CDATA block. + * + * @return The string up to the ]]>. + * @throws JSONException If the ]]> is not found. + */ + public String nextCDATA() throws JSONException { + char c; + int i; + StringBuilder sb = new StringBuilder(); + for (;;) { + c = next(); + if (end()) { + throw syntaxError("Unclosed CDATA"); + } + sb.append(c); + i = sb.length() - 3; + if (i >= 0 && sb.charAt(i) == ']' && sb.charAt(i + 1) == ']' && sb.charAt(i + 2) == '>') { + sb.setLength(i); + return sb.toString(); + } + } + } + + /** + * Get the next XML outer token, trimming whitespace. + * There are two kinds of tokens: the '>' character which begins a markup tag, and the content text between markup tags. + * + * @return A string, or a '>' Character, or null if there is no more source text. + * @throws JSONException JSON + */ + public Object nextContent() throws JSONException { + char c; + StringBuilder sb; + do { + c = next(); + } while (Character.isWhitespace(c)); + if (c == 0) { + return null; + } + if (c == '<') { + return XML.LT; + } + sb = new StringBuilder(); + for (;;) { + if (c == '<' || c == 0) { + back(); + return sb.toString().trim(); + } + if (c == '&') { + sb.append(nextEntity(c)); + } else { + sb.append(c); + } + c = next(); + } + } + + /** + * Return the next entity. These entities are translated to Characters: & ' > < ". + * + * @param ampersand An ampersand character. + * @return A Character or an entity String if the entity is not recognized. + * @throws JSONException If missing ';' in XML entity. + */ + public Object nextEntity(char ampersand) throws JSONException { + StringBuilder sb = new StringBuilder(); + for (;;) { + char c = next(); + if (Character.isLetterOrDigit(c) || c == '#') { + sb.append(Character.toLowerCase(c)); + } else if (c == ';') { + break; + } else { + throw syntaxError("Missing ';' in XML entity: &" + sb); + } + } + String string = sb.toString(); + Object object = entity.get(string); + return object != null ? object : ampersand + string + ";"; + } + + /** + * Returns the next XML meta token. This is used for skipping over <!...> and <?...?> structures. + * + * @return Syntax characters (< > / = ! ?) are returned as Character, and strings and names are returned as Boolean. We don't care what the values actually are. + * @throws JSONException 字符串中属性未关闭或XML结构错误抛出此异常。If a string is not properly closed or if the XML is badly structured. + */ + public Object nextMeta() throws JSONException { + char c; + char q; + do { + c = next(); + } while (Character.isWhitespace(c)); + switch (c) { + case 0: + throw syntaxError("Misshaped meta tag"); + case '<': + return XML.LT; + case '>': + return XML.GT; + case '/': + return XML.SLASH; + case '=': + return XML.EQ; + case '!': + return XML.BANG; + case '?': + return XML.QUEST; + case '"': + case '\'': + q = c; + for (;;) { + c = next(); + if (c == 0) { + throw syntaxError("Unterminated string"); + } + if (c == q) { + return Boolean.TRUE; + } + } + default: + for (;;) { + c = next(); + if (Character.isWhitespace(c)) { + return Boolean.TRUE; + } + switch (c) { + case 0: + case '<': + case '>': + case '/': + case '=': + case '!': + case '?': + case '"': + case '\'': + back(); + return Boolean.TRUE; + } + } + } + } + + /** + * Get the next XML Token. These tokens are found inside of angle brackets. It may be one of these characters: / > = ! ? or it may be a string wrapped in single quotes or double + * quotes, or it may be a name. + * + * @return a String or a Character. + * @throws JSONException If the XML is not well formed. + */ + public Object nextToken() throws JSONException { + char c; + char q; + StringBuilder sb; + do { + c = next(); + } while (Character.isWhitespace(c)); + switch (c) { + case 0: + throw syntaxError("Misshaped element"); + case '<': + throw syntaxError("Misplaced '<'"); + case '>': + return XML.GT; + case '/': + return XML.SLASH; + case '=': + return XML.EQ; + case '!': + return XML.BANG; + case '?': + return XML.QUEST; + + // Quoted string + + case '"': + case '\'': + q = c; + sb = new StringBuilder(); + for (;;) { + c = next(); + if (c == 0) { + throw syntaxError("Unterminated string"); + } + if (c == q) { + return sb.toString(); + } + if (c == '&') { + sb.append(nextEntity(c)); + } else { + sb.append(c); + } + } + default: + + // Name + + sb = new StringBuilder(); + for (;;) { + sb.append(c); + c = next(); + if (Character.isWhitespace(c)) { + return sb.toString(); + } + switch (c) { + case 0: + return sb.toString(); + case '>': + case '/': + case '=': + case '!': + case '?': + case '[': + case ']': + back(); + return sb.toString(); + case '<': + case '"': + case '\'': + throw syntaxError("Bad character in a name"); + } + } + } + } + + /** + * Skip characters until past the requested string. If it is not found, we are left at the end of the source with a result of false. + * + * @param to A string to skip past. + * @return 是否成功skip + * @throws JSONException JSON异常 + */ + public boolean skipPast(String to) throws JSONException { + boolean b; + char c; + int i; + int j; + int offset = 0; + int length = to.length(); + char[] circle = new char[length]; + + /* + * First fill the circle buffer with as many characters as are in the to string. If we reach an early end, bail. + */ + + for (i = 0; i < length; i += 1) { + c = next(); + if (c == 0) { + return false; + } + circle[i] = c; + } + + /* We will loop, possibly for all of the remaining characters. */ + + for (;;) { + j = offset; + b = true; + + /* Compare the circle buffer with the to string. */ + + for (i = 0; i < length; i += 1) { + if (circle[j] != to.charAt(i)) { + b = false; + break; + } + j += 1; + if (j >= length) { + j -= length; + } + } + + /* If we exit the loop with b intact, then victory is ours. */ + + if (b) { + return true; + } + + /* Get the next character. If there isn't one, then defeat is ours. */ + + c = next(); + if (c == 0) { + return false; + } + /* + * Shove the character in the circle buffer and advance the circle offset. The offset is mod n. + */ + circle[offset] = c; + offset += 1; + if (offset >= length) { + offset -= length; + } + } + } +} diff --git a/hutool-json/src/main/java/cn/hutool/json/package-info.java b/hutool-json/src/main/java/cn/hutool/json/package-info.java new file mode 100644 index 000000000..b8ba7d252 --- /dev/null +++ b/hutool-json/src/main/java/cn/hutool/json/package-info.java @@ -0,0 +1,7 @@ +/** + * JSON封装,基于json.org官方库改造 + * + * @author looly + * + */ +package cn.hutool.json; \ No newline at end of file diff --git a/hutool-json/src/test/java/cn/hutool/json/JSONArrayTest.java b/hutool-json/src/test/java/cn/hutool/json/JSONArrayTest.java new file mode 100644 index 000000000..35378ceba --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/JSONArrayTest.java @@ -0,0 +1,199 @@ +package cn.hutool.json; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.lang.Console; +import cn.hutool.core.lang.Dict; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.json.test.bean.Exam; +import cn.hutool.json.test.bean.JsonNode; +import cn.hutool.json.test.bean.KeyBean; + +/** + * JSONArray单元测试 + * + * @author Looly + * + */ +public class JSONArrayTest { + + @Test + public void addTest() { + // 方法1 + JSONArray array = JSONUtil.createArray(); + // 方法2 + // JSONArray array = new JSONArray(); + array.add("value1"); + array.add("value2"); + array.add("value3"); + + Assert.assertEquals(array.get(0), "value1"); + } + + @Test + public void parseTest() { + String jsonStr = "[\"value1\", \"value2\", \"value3\"]"; + JSONArray array = JSONUtil.parseArray(jsonStr); + Assert.assertEquals(array.get(0), "value1"); + } + + @Test + public void parseFileTest() { + JSONArray array = JSONUtil.readJSONArray(FileUtil.file("exam_test.json"), CharsetUtil.CHARSET_UTF_8); + + JSONObject obj0 = array.getJSONObject(0); + Exam exam = JSONUtil.toBean(obj0, Exam.class); + Assert.assertEquals("0", exam.getAnswerArray()[0].getSeq()); + } + + @Test + @Ignore + public void parseBeanListTest() { + KeyBean b1 = new KeyBean(); + b1.setAkey("aValue1"); + b1.setBkey("bValue1"); + KeyBean b2 = new KeyBean(); + b2.setAkey("aValue2"); + b2.setBkey("bValue2"); + + ArrayList list = CollUtil.newArrayList(b1, b2); + + JSONArray jsonArray = JSONUtil.parseArray(list); + Console.log(jsonArray); + } + + @Test + public void toListTest() { + String jsonStr = FileUtil.readString("exam_test.json", CharsetUtil.CHARSET_UTF_8); + JSONArray array = JSONUtil.parseArray(jsonStr); + + List list = array.toList(Exam.class); + Assert.assertFalse(list.isEmpty()); + Assert.assertEquals(Exam.class, list.get(0).getClass()); + ; + } + + @Test + public void toListTest2() { + String jsonArr = "[{\"id\":111,\"name\":\"test1\"},{\"id\":112,\"name\":\"test2\"}]"; + + JSONArray array = JSONUtil.parseArray(jsonArr); + List userList = JSONUtil.toList(array, User.class); + + Assert.assertFalse(userList.isEmpty()); + Assert.assertEquals(User.class, userList.get(0).getClass()); + + Assert.assertEquals(Integer.valueOf(111), userList.get(0).getId()); + Assert.assertEquals(Integer.valueOf(112), userList.get(1).getId()); + + Assert.assertEquals("test1", userList.get(0).getName()); + Assert.assertEquals("test2", userList.get(1).getName()); + } + + @Test + public void toDictListTest() { + String jsonArr = "[{\"id\":111,\"name\":\"test1\"},{\"id\":112,\"name\":\"test2\"}]"; + + JSONArray array = JSONUtil.parseArray(jsonArr); + + List list = JSONUtil.toList(array, Dict.class); + + Assert.assertFalse(list.isEmpty()); + Assert.assertEquals(Dict.class, list.get(0).getClass()); + + Assert.assertEquals(Integer.valueOf(111), list.get(0).getInt("id")); + Assert.assertEquals(Integer.valueOf(112), list.get(1).getInt("id")); + + Assert.assertEquals("test1", list.get(0).getStr("name")); + Assert.assertEquals("test2", list.get(1).getStr("name")); + } + + @Test + public void toArrayTest() { + String jsonStr = FileUtil.readString("exam_test.json", CharsetUtil.CHARSET_UTF_8); + JSONArray array = JSONUtil.parseArray(jsonStr); + + Exam[] list = array.toArray(new Exam[0]); + Assert.assertFalse(0 == list.length); + Assert.assertEquals(Exam.class, list[0].getClass()); + } + + /** + * 单元测试用于测试在列表元素中有null时的情况下是否出错 + */ + @Test + public void toListWithNullTest() { + String json = "[null,{'akey':'avalue','bkey':'bvalue'}]"; + JSONArray ja = JSONUtil.parseArray(json); + + List list = ja.toList(KeyBean.class); + Assert.assertTrue(null == list.get(0)); + Assert.assertEquals("avalue", list.get(1).getAkey()); + Assert.assertEquals("bvalue", list.get(1).getBkey()); + } + + @Test + public void toBeanListTest() { + List> mapList = new ArrayList<>(); + mapList.add(buildMap("0", "0", "0")); + mapList.add(buildMap("1", "1", "1")); + mapList.add(buildMap("+0", "+0", "+0")); + mapList.add(buildMap("-0", "-0", "-0")); + JSONArray jsonArray = JSONUtil.parseArray(mapList); + List nodeList = jsonArray.toList(JsonNode.class); + + Assert.assertEquals(Long.valueOf(0L), nodeList.get(0).getId()); + Assert.assertEquals(Long.valueOf(1L), nodeList.get(1).getId()); + Assert.assertEquals(Long.valueOf(0L), nodeList.get(2).getId()); + Assert.assertEquals(Long.valueOf(0L), nodeList.get(3).getId()); + + Assert.assertEquals(Integer.valueOf(0), nodeList.get(0).getParentId()); + Assert.assertEquals(Integer.valueOf(1), nodeList.get(1).getParentId()); + Assert.assertEquals(Integer.valueOf(0), nodeList.get(2).getParentId()); + Assert.assertEquals(Integer.valueOf(0), nodeList.get(3).getParentId()); + + Assert.assertEquals("0", nodeList.get(0).getName()); + Assert.assertEquals("1", nodeList.get(1).getName()); + Assert.assertEquals("+0", nodeList.get(2).getName()); + Assert.assertEquals("-0", nodeList.get(3).getName()); + } + + private static Map buildMap(String id, String parentId, String name) { + Map map = new HashMap<>(); + map.put("id", id); + map.put("parentId", parentId); + map.put("name", name); + return map; + } + + class User { + private Integer id; + private String name; + + public Integer getId() { + return id; + } + public void setId(Integer id) { + this.id = id; + } + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + @Override + public String toString() { + return "User [id=" + id + ", name=" + name + "]"; + } + } +} diff --git a/hutool-json/src/test/java/cn/hutool/json/JSONConvertTest.java b/hutool-json/src/test/java/cn/hutool/json/JSONConvertTest.java new file mode 100644 index 000000000..9cc8276e9 --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/JSONConvertTest.java @@ -0,0 +1,106 @@ +package cn.hutool.json; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.hutool.json.test.bean.ExamInfoDict; +import cn.hutool.json.test.bean.PerfectEvaluationProductResVo; +import cn.hutool.json.test.bean.UserInfoDict; + +/** + * JSON转换单元测试 + * + * @author Looly,质量过关 + * + */ +public class JSONConvertTest { + + @Test + public void testBean2Json() { + + UserInfoDict userInfoDict = new UserInfoDict(); + userInfoDict.setId(1); + userInfoDict.setPhotoPath("yx.mm.com"); + userInfoDict.setRealName("质量过关"); + + ExamInfoDict examInfoDict = new ExamInfoDict(); + examInfoDict.setId(1); + examInfoDict.setExamType(0); + examInfoDict.setAnswerIs(1); + + ExamInfoDict examInfoDict1 = new ExamInfoDict(); + examInfoDict1.setId(2); + examInfoDict1.setExamType(0); + examInfoDict1.setAnswerIs(0); + + ExamInfoDict examInfoDict2 = new ExamInfoDict(); + examInfoDict2.setId(3); + examInfoDict2.setExamType(1); + examInfoDict2.setAnswerIs(0); + + List examInfoDicts = new ArrayList(); + examInfoDicts.add(examInfoDict); + examInfoDicts.add(examInfoDict1); + examInfoDicts.add(examInfoDict2); + + userInfoDict.setExamInfoDict(examInfoDicts); + + Map tempMap = new HashMap(); + tempMap.put("userInfoDict", userInfoDict); + tempMap.put("toSendManIdCard", 1); + + JSONObject obj = JSONUtil.parseObj(tempMap); + Assert.assertEquals(new Integer(1), obj.getInt("toSendManIdCard")); + + JSONObject examInfoDictsJson = obj.getJSONObject("userInfoDict"); + Assert.assertEquals(new Integer(1), examInfoDictsJson.getInt("id")); + Assert.assertEquals("质量过关", examInfoDictsJson.getStr("realName")); + + Object id = JSONUtil.getByPath(obj, "userInfoDict.examInfoDict[0].id"); + Assert.assertEquals(1, id); + } + + @Test + public void testJson2Bean() { + // language=JSON + String examJson = "{\n" + " \"examInfoDicts\": {\n" + " \"id\": 1,\n" + " \"realName\": \"质量过关\",\n" // + + " \"examInfoDict\": [\n" + " {\n" + " \"id\": 1,\n" + " \"answerIs\": 1,\n" + " \"examType\": 0\n" // + + " },\n" + " {\n" + " \"id\": 2,\n" + " \"answerIs\": 0,\n" + " \"examType\": 0\n" + " },\n" // + + " {\n" + " \"id\": 3,\n" + " \"answerIs\": 0,\n" + " \"examType\": 1\n" + " }\n" + " ],\n" // + + " \"photoPath\": \"yx.mm.com\"\n" + " },\n" + " \"toSendManIdCard\": 1\n" + "}"; + + JSONObject jsonObject = JSONUtil.parseObj(examJson).getJSONObject("examInfoDicts"); + UserInfoDict userInfoDict = jsonObject.toBean(UserInfoDict.class); + + Assert.assertEquals(userInfoDict.getId(), new Integer(1)); + Assert.assertEquals(userInfoDict.getRealName(), "质量过关"); + + //============ + + String jsonStr = "{\"id\":null,\"examInfoDict\":[{\"answerIs\":1, \"id\":null}]}";//JSONUtil.toJsonStr(userInfoDict1); + JSONObject jsonObject2 = JSONUtil.parseObj(jsonStr);//.getJSONObject("examInfoDicts"); + UserInfoDict userInfoDict2 = jsonObject2.toBean(UserInfoDict.class); + Assert.assertNull(userInfoDict2.getId()); + } + + /** + * 针对Bean中Setter返回this测试是否可以成功调用Setter方法并注入 + */ + @Test + public void testJson2Bean2() { + String jsonStr = ResourceUtil.readUtf8Str("evaluation.json"); + JSONObject obj = JSONUtil.parseObj(jsonStr); + PerfectEvaluationProductResVo vo = obj.toBean(PerfectEvaluationProductResVo.class); + + Assert.assertEquals(obj.getStr("HA001"), vo.getHA001()); + Assert.assertEquals(obj.getInt("costTotal"), vo.getCostTotal()); + } +} \ No newline at end of file diff --git a/hutool-json/src/test/java/cn/hutool/json/JSONObjectTest.java b/hutool-json/src/test/java/cn/hutool/json/JSONObjectTest.java new file mode 100644 index 000000000..d196c4c6e --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/JSONObjectTest.java @@ -0,0 +1,460 @@ +package cn.hutool.json; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.lang.Console; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.test.bean.JSONBean; +import cn.hutool.json.test.bean.ResultDto; +import cn.hutool.json.test.bean.Seq; +import cn.hutool.json.test.bean.TokenAuthResponse; +import cn.hutool.json.test.bean.TokenAuthWarp2; +import cn.hutool.json.test.bean.UserA; +import cn.hutool.json.test.bean.UserB; +import cn.hutool.json.test.bean.UserWithMap; +import cn.hutool.json.test.bean.report.CaseReport; +import cn.hutool.json.test.bean.report.StepReport; +import cn.hutool.json.test.bean.report.SuiteReport; + +/** + * JSONObject单元测试 + * + * @author Looly + * + */ +public class JSONObjectTest { + + @Test + @Ignore + public void toStringTest() { + String str = "{\"code\": 500, \"data\":null}"; + JSONObject jsonObject = new JSONObject(str); + Console.log(jsonObject); + jsonObject.getConfig().setIgnoreNullValue(true); + Console.log(jsonObject.toStringPretty()); + } + + @Test + public void toStringTest2() { + String str = "{\"test\":\"关于开展2018年度“文明集体”、“文明职工”评选表彰活动的通知\"}"; + JSONObject json = new JSONObject(str); + Assert.assertEquals(str, json.toString()); + } + + /** + * 测试JSON中自定义日期格式输出正确性 + */ + @Test + public void toStringTest3() { + JSONObject json = JSONUtil.createObj()// + .put("dateTime", DateUtil.parse("2019-05-02 22:12:01"))// + .setDateFormat(DatePattern.NORM_DATE_PATTERN); + Assert.assertEquals("{\"dateTime\":\"2019-05-02\"}", json.toString()); + } + + @Test + public void toStringWithDateTest() { + JSONObject json = JSONUtil.createObj().put("date", DateUtil.parse("2019-05-08 19:18:21")); + Assert.assertEquals("{\"date\":1557314301000}", json.toString()); + + json = JSONUtil.createObj().put("date", DateUtil.parse("2019-05-08 19:18:21")).setDateFormat(DatePattern.NORM_DATE_PATTERN); + Assert.assertEquals("{\"date\":\"2019-05-08\"}", json.toString()); + } + + @Test + public void putAllTest() { + JSONObject json1 = JSONUtil.createObj(); + json1.put("a", "value1"); + json1.put("b", "value2"); + json1.put("c", "value3"); + json1.put("d", true); + + JSONObject json2 = JSONUtil.createObj(); + json2.put("a", "value21"); + json2.put("b", "value22"); + + // putAll操作会覆盖相同key的值,因此a,b两个key的值改变,c的值不变 + json1.putAll(json2); + + Assert.assertEquals(json1.get("a"), "value21"); + Assert.assertEquals(json1.get("b"), "value22"); + Assert.assertEquals(json1.get("c"), "value3"); + } + + @Test + public void parseStringTest() { + String jsonStr = "{\"b\":\"value2\",\"c\":\"value3\",\"a\":\"value1\", \"d\": true, \"e\": null}"; + JSONObject jsonObject = JSONUtil.parseObj(jsonStr); + Assert.assertEquals(jsonObject.get("a"), "value1"); + Assert.assertEquals(jsonObject.get("b"), "value2"); + Assert.assertEquals(jsonObject.get("c"), "value3"); + Assert.assertEquals(jsonObject.get("d"), true); + + Assert.assertTrue(jsonObject.containsKey("e")); + Assert.assertEquals(jsonObject.get("e"), JSONNull.NULL); + } + + @Test + public void parseStringTest2() { + String jsonStr = "{\"file_name\":\"RMM20180127009_731.000\",\"error_data\":\"201121151350701001252500000032 18973908335 18973908335 13601893517 201711211700152017112115135420171121 6594000000010100000000000000000000000043190101701001910072 100001100 \",\"error_code\":\"F140\",\"error_info\":\"最早发送时间格式错误,该字段可以为空,当不为空时正确填写格式为“YYYYMMDDHHMISS”\",\"app_name\":\"inter-pre-check\"}"; + JSONObject json = new JSONObject(jsonStr); + Assert.assertEquals("F140", json.getStr("error_code")); + Assert.assertEquals("最早发送时间格式错误,该字段可以为空,当不为空时正确填写格式为“YYYYMMDDHHMISS”", json.getStr("error_info")); + } + + @Test + public void parseStringTest3() { + String jsonStr = "{\"test\":\"体”、“文\"}"; + JSONObject json = new JSONObject(jsonStr); + Assert.assertEquals("体”、“文", json.getStr("test")); + } + + @Test + @Ignore + public void parseStringWithBomTest() { + String jsonStr = FileUtil.readUtf8String("f:/test/jsontest.txt"); + JSONObject json = new JSONObject(jsonStr); + JSONObject json2 = JSONUtil.parseObj(json); + Console.log(json); + Console.log(json2); + } + + @Test + public void toBeanTest() { + JSONObject subJson = JSONUtil.createObj().put("value1", "strValue1").put("value2", "234"); + JSONObject json = JSONUtil.createObj().put("strValue", "strTest").put("intValue", 123) + // 测试空字符串转对象 + .put("doubleValue", "").put("beanValue", subJson).put("list", JSONUtil.createArray().put("a").put("b")).put("testEnum", "TYPE_A"); + + TestBean bean = json.toBean(TestBean.class); + Assert.assertEquals("a", bean.getList().get(0)); + Assert.assertEquals("b", bean.getList().get(1)); + + Assert.assertEquals("strValue1", bean.getBeanValue().getValue1()); + // BigDecimal转换检查 + Assert.assertEquals(new BigDecimal("234"), bean.getBeanValue().getValue2()); + // 枚举转换检查 + Assert.assertEquals(TestEnum.TYPE_A, bean.getTestEnum()); + } + + @Test + public void toBeanNullStrTest() { + JSONObject json = JSONUtil.createObj()// + .put("strValue", "null")// + .put("intValue", 123)// + .put("beanValue", "null")// + .put("list", JSONUtil.createArray().put("a").put("b")); + + TestBean bean = json.toBean(TestBean.class); + // 当JSON中为字符串"null"时应被当作字符串处理 + Assert.assertEquals("null", bean.getStrValue()); + // 当JSON中为字符串"null"时Bean中的字段类型不匹配应在ignoreError模式下忽略注入 + Assert.assertEquals(null, bean.getBeanValue()); + } + + @Test + public void toBeanTest2() { + UserA userA = new UserA(); + userA.setA("A user"); + userA.setName("{\n\t\"body\":{\n\t\t\"loginId\":\"id\",\n\t\t\"password\":\"pwd\"\n\t}\n}"); + userA.setDate(new Date()); + userA.setSqs(CollectionUtil.newArrayList(new Seq("seq1"), new Seq("seq2"))); + + JSONObject json = JSONUtil.parseObj(userA); + UserA userA2 = json.toBean(UserA.class); + // 测试数组 + Assert.assertEquals("seq1", userA2.getSqs().get(0).getSeq()); + // 测试带换行符等特殊字符转换是否成功 + Assert.assertTrue(StrUtil.isNotBlank(userA2.getName())); + } + + @Test + public void toBeanTest3() { + String jsonStr = "{'data':{'userName':'ak','password': null}}"; + UserWithMap user = JSONUtil.toBean(JSONUtil.parseObj(jsonStr), UserWithMap.class); + String password = user.getData().get("password"); + Assert.assertTrue(user.getData().containsKey("password")); + Assert.assertNull(password); + } + + @Test + public void toBeanTest4() { + String json = "{\"data\":{\"b\": \"c\"}}"; + + UserWithMap map = JSONUtil.toBean(json, UserWithMap.class); + Assert.assertEquals("c", map.getData().get("b")); + } + + @Test + public void toBeanTest5() { + String readUtf8Str = ResourceUtil.readUtf8Str("suiteReport.json"); + JSONObject json = JSONUtil.parseObj(readUtf8Str); + SuiteReport bean = json.toBean(SuiteReport.class); + + // 第一层 + List caseReports = bean.getCaseReports(); + CaseReport caseReport = caseReports.get(0); + Assert.assertNotNull(caseReport); + + // 第二层 + List stepReports = caseReports.get(0).getStepReports(); + StepReport stepReport = stepReports.get(0); + Assert.assertNotNull(stepReport); + } + + /** + * 在JSON转Bean过程中,Bean中字段如果为父类定义的泛型类型,则应正确转换,此方法用于测试这类情况 + */ + @Test + public void toBeanTest6() { + JSONObject json = JSONUtil.createObj().put("targetUrl", "http://test.com").put("success", "true").put("result", JSONUtil.createObj().put("token", "tokenTest").put("userId", "测试用户1")); + + TokenAuthWarp2 bean = json.toBean(TokenAuthWarp2.class); + Assert.assertEquals("http://test.com", bean.getTargetUrl()); + Assert.assertEquals("true", bean.getSuccess()); + + TokenAuthResponse result = bean.getResult(); + Assert.assertNotNull(result); + Assert.assertEquals("tokenTest", result.getToken()); + Assert.assertEquals("测试用户1", result.getUserId()); + } + + /** + * 泛型对象中的泛型参数如果未定义具体类型,按照JSON处理
+ * 此处用于测试获取泛型类型实际类型错误导致的空指针问题 + */ + @Test + public void toBeanTest7() { + String jsonStr = " {\"result\":{\"phone\":\"15926297342\",\"appKey\":\"e1ie12e1ewsdqw1\",\"secret\":\"dsadadqwdqs121d1e2\",\"message\":\"hello world\"},\"code\":100,\"message\":\"validate message\"}"; + ResultDto dto = JSONUtil.toBean(jsonStr, ResultDto.class); + Assert.assertEquals("validate message", dto.getMessage()); + } + + @Test + public void parseBeanTest() { + UserA userA = new UserA(); + userA.setName("nameTest"); + userA.setDate(new Date()); + userA.setSqs(CollectionUtil.newArrayList(new Seq(null), new Seq("seq2"))); + + JSONObject json = JSONUtil.parseObj(userA, false); + Assert.assertTrue(json.containsKey("a")); + Assert.assertTrue(json.getJSONArray("sqs").getJSONObject(0).containsKey("seq")); + } + + @Test + public void parseBeanTest2() { + TestBean bean = new TestBean(); + bean.setDoubleValue(111.1); + bean.setIntValue(123); + bean.setList(CollUtil.newArrayList("a", "b", "c")); + bean.setStrValue("strTest"); + bean.setTestEnum(TestEnum.TYPE_B); + + JSONObject json = JSONUtil.parseObj(bean, false); + // 枚举转换检查 + Assert.assertEquals("TYPE_B", json.get("testEnum")); + + TestBean bean2 = json.toBean(TestBean.class); + Assert.assertEquals(bean.toString(), bean2.toString()); + } + + @Test + public void parseBeanTest3() { + JSONObject json = JSONUtil.createObj().put("code", 22).put("data", "{\"jobId\": \"abc\", \"videoUrl\": \"http://a.com/a.mp4\"}"); + + JSONBean bean = json.toBean(JSONBean.class); + Assert.assertEquals(22, bean.getCode()); + Assert.assertEquals("abc", bean.getData().getObj("jobId")); + Assert.assertEquals("http://a.com/a.mp4", bean.getData().getObj("videoUrl")); + } + + @Test + public void beanTransTest() { + UserA userA = new UserA(); + userA.setA("A user"); + userA.setName("nameTest"); + userA.setDate(new Date()); + + JSONObject userAJson = JSONUtil.parseObj(userA); + UserB userB = JSONUtil.toBean(userAJson, UserB.class); + + Assert.assertEquals(userA.getName(), userB.getName()); + Assert.assertEquals(userA.getDate(), userB.getDate()); + } + + @Test + public void beanTransTest2() { + UserA userA = new UserA(); + userA.setA("A user"); + userA.setName("nameTest"); + userA.setDate(DateUtil.parse("2018-10-25")); + + JSONObject userAJson = JSONUtil.parseObj(userA); + // 自定义日期格式 + userAJson.setDateFormat("yyyy-MM-dd"); + + UserA bean = JSONUtil.toBean(userAJson.toString(), UserA.class); + Assert.assertEquals(DateUtil.parse("2018-10-25"), bean.getDate()); + } + + @Test + public void beanTransTest3() { + JSONObject userAJson = JSONUtil.createObj().put("a", "AValue").put("name", "nameValue").put("date", "08:00:00"); + UserA bean = JSONUtil.toBean(userAJson.toString(), UserA.class); + Assert.assertEquals(DateUtil.today() + " 08:00:00", DateUtil.date(bean.getDate()).toString()); + } + + @Test + public void parseFromBeanTest() { + UserA userA = new UserA(); + userA.setA(null); + userA.setName("nameTest"); + userA.setDate(new Date()); + + JSONObject userAJson = JSONUtil.parseObj(userA); + Assert.assertFalse(userAJson.containsKey("a")); + + JSONObject userAJsonWithNullValue = JSONUtil.parseObj(userA, false); + Assert.assertTrue(userAJsonWithNullValue.containsKey("a")); + Assert.assertTrue(userAJsonWithNullValue.containsKey("sqs")); + } + + @Test + public void specialCharTest() { + String json = "{\"pattern\": \"[abc]\b\u2001\", \"pattern2Json\": {\"patternText\": \"[ab]\\b\"}}"; + JSONObject obj = JSONUtil.parseObj(json); + Assert.assertEquals("[abc]\\b\\u2001", obj.getStrEscaped("pattern")); + Assert.assertEquals("{\"patternText\":\"[ab]\\b\"}", obj.getStrEscaped("pattern2Json")); + } + + @Test + public void getStrTest() { + String json = "{\"name\": \"yyb\\nbbb\"}"; + JSONObject jsonObject = JSONUtil.parseObj(json); + + // 没有转义按照默认规则显示 + Assert.assertEquals("yyb\nbbb", jsonObject.getStr("name")); + // 转义按照字符串显示 + Assert.assertEquals("yyb\\nbbb", jsonObject.getStrEscaped("name")); + + String bbb = jsonObject.getStr("bbb", "defaultBBB"); + Console.log(bbb); + } + + public static enum TestEnum { + TYPE_A, TYPE_B + } + + /** + * 测试Bean + * + * @author Looly + * + */ + public static class TestBean { + private String strValue; + private int intValue; + private Double doubleValue; + private subBean beanValue; + private List list; + private TestEnum testEnum; + + public String getStrValue() { + return strValue; + } + + public void setStrValue(String strValue) { + this.strValue = strValue; + } + + public int getIntValue() { + return intValue; + } + + public void setIntValue(int intValue) { + this.intValue = intValue; + } + + public Double getDoubleValue() { + return doubleValue; + } + + public void setDoubleValue(Double doubleValue) { + this.doubleValue = doubleValue; + } + + public subBean getBeanValue() { + return beanValue; + } + + public void setBeanValue(subBean beanValue) { + this.beanValue = beanValue; + } + + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } + + public TestEnum getTestEnum() { + return testEnum; + } + + public void setTestEnum(TestEnum testEnum) { + this.testEnum = testEnum; + } + + @Override + public String toString() { + return "TestBean [strValue=" + strValue + ", intValue=" + intValue + ", doubleValue=" + doubleValue + ", beanValue=" + beanValue + ", list=" + list + ", testEnum=" + testEnum + "]"; + } + } + + /** + * 测试子Bean + * + * @author Looly + * + */ + public static class subBean { + private String value1; + private BigDecimal value2; + + public String getValue1() { + return value1; + } + + public void setValue1(String value1) { + this.value1 = value1; + } + + public BigDecimal getValue2() { + return value2; + } + + public void setValue2(BigDecimal value2) { + this.value2 = value2; + } + + @Override + public String toString() { + return "subBean [value1=" + value1 + ", value2=" + value2 + "]"; + } + } +} diff --git a/hutool-json/src/test/java/cn/hutool/json/JSONPathTest.java b/hutool-json/src/test/java/cn/hutool/json/JSONPathTest.java new file mode 100644 index 000000000..daca707b9 --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/JSONPathTest.java @@ -0,0 +1,22 @@ +package cn.hutool.json; + +import org.junit.Assert; +import org.junit.Test; + +/** + * JSON路径单元测试 + * + * @author looly + * + */ +public class JSONPathTest { + + @Test + public void getByPathTest() { + String json = "[{\"id\":\"1\",\"name\":\"xingming\"},{\"id\":\"2\",\"name\":\"mingzi\"}]"; + Object value = JSONUtil.parseArray(json).getByPath("[0].name"); + Assert.assertEquals("xingming", value); + value = JSONUtil.parseArray(json).getByPath("[1].name"); + Assert.assertEquals("mingzi", value); + } +} diff --git a/hutool-json/src/test/java/cn/hutool/json/JSONStrFormaterTest.java b/hutool-json/src/test/java/cn/hutool/json/JSONStrFormaterTest.java new file mode 100644 index 000000000..e9450f10e --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/JSONStrFormaterTest.java @@ -0,0 +1,33 @@ +package cn.hutool.json; + +import org.junit.Assert; +import org.junit.Test; + +/** + * JSON字符串格式化单元测试 + * @author looly + * + */ +public class JSONStrFormaterTest { + + @Test + public void formatTest() { + String json = "{'age':23,'aihao':['pashan','movies'],'name':{'firstName':'zhang','lastName':'san','aihao':['pashan','movies','name':{'firstName':'zhang','lastName':'san','aihao':['pashan','movies']}]}}"; + String result = JSONStrFormater.format(json); + Assert.assertNotNull(result); + } + + @Test + public void formatTest2() { + String json = "{\"abc\":{\"def\":\"\\\"[ghi]\"}}"; + String result = JSONStrFormater.format(json); + Assert.assertNotNull(result); + } + + @Test + public void formatTest3() { + String json = "{\"id\":13,\"title\":\"《标题》\",\"subtitle\":\"副标题z'c'z'xv'c'xv\",\"user_id\":6,\"type\":0}"; + String result = JSONStrFormater.format(json); + Assert.assertNotNull(result); + } +} diff --git a/hutool-json/src/test/java/cn/hutool/json/JSONUtilTest.java b/hutool-json/src/test/java/cn/hutool/json/JSONUtilTest.java new file mode 100644 index 000000000..fc76a31c3 --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/JSONUtilTest.java @@ -0,0 +1,130 @@ +package cn.hutool.json; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.json.test.bean.Price; +import cn.hutool.json.test.bean.UserA; +import cn.hutool.json.test.bean.UserC; + +public class JSONUtilTest { + + @Test + public void toJsonStrTest() { + UserA a1 = new UserA(); + a1.setA("aaaa"); + a1.setDate(DateUtil.date()); + a1.setName("AAAAName"); + UserA a2 = new UserA(); + a2.setA("aaaa222"); + a2.setDate(DateUtil.date()); + a2.setName("AAAA222Name"); + + ArrayList list = CollectionUtil.newArrayList(a1, a2); + HashMap map = CollectionUtil.newHashMap(); + map.put("total", 13); + map.put("rows", list); + + String str = JSONUtil.toJsonPrettyStr(map); + Assert.assertNotNull(str); + } + + @Test + public void toJsonStrTest2() { + Map model = new HashMap(); + model.put("mobile", "17610836523"); + model.put("type", 1); + + Map data = new HashMap(); + data.put("model", model); + data.put("model2", model); + + JSONObject jsonObject = JSONUtil.parseObj(data); + + Assert.assertTrue(jsonObject.containsKey("model")); + Assert.assertEquals(1, jsonObject.getJSONObject("model").getInt("type").intValue()); + Assert.assertEquals("17610836523", jsonObject.getJSONObject("model").getStr("mobile")); + // Assert.assertEquals("{\"model\":{\"type\":1,\"mobile\":\"17610836523\"}}", jsonObject.toString()); + } + + @Test + public void toJsonStrTest3() { + // 验证某个字段为JSON字符串时转义是否规范 + JSONObject object = new JSONObject(true); + object.put("name", "123123"); + object.put("value", "\\"); + object.put("value2", " map = MapUtil.newHashMap(); + map.put("user", object.toString()); + + JSONObject json = JSONUtil.parseObj(map); + Assert.assertEquals("{\"name\":\"123123\",\"value\":\"\\\\\",\"value2\":\"<\\/\"}", json.get("user")); + Assert.assertEquals("{\"user\":\"{\\\"name\\\":\\\"123123\\\",\\\"value\\\":\\\"\\\\\\\\\\\",\\\"value2\\\":\\\"<\\\\/\\\"}\"}", json.toString()); + + JSONObject json2 = JSONUtil.parseObj(json.toString()); + Assert.assertEquals("{\"name\":\"123123\",\"value\":\"\\\\\",\"value2\":\"<\\/\"}", json2.get("user")); + } + + /** + * 泛型多层嵌套测试 + */ + @Test + public void toBeanTest() { + String json = "{\"ADT\":[[{\"BookingCode\":[\"N\",\"N\"]}]]}"; + + Price price = JSONUtil.toBean(json, Price.class); + Assert.assertEquals("N", price.getADT().get(0).get(0).getBookingCode().get(0)); + } + + @Test + public void toBeanTest2() { + // 测试JSONObject转为Bean中字符串字段的情况 + String json = "{\"id\":123,\"name\":\"张三\",\"prop\":{\"gender\":\"男\", \"age\":18}}"; + UserC user = JSONUtil.toBean(json, UserC.class); + Assert.assertNotNull(user.getProp()); + String prop = user.getProp(); + JSONObject propJson = JSONUtil.parseObj(prop); + Assert.assertEquals("男", propJson.getStr("gender")); + Assert.assertEquals(18, propJson.getInt("age").intValue()); +// Assert.assertEquals("{\"age\":18,\"gender\":\"男\"}", user.getProp()); + } + + @Test + public void putByPathTest() { + JSONObject json = new JSONObject(); + json.putByPath("aa.bb", "BB"); + Assert.assertEquals("{\"aa\":{\"bb\":\"BB\"}}", json.toString()); + } + + @Test + public void getStrTest() { + String html = "{\"name\":\"Something must have been changed since you leave\"}"; + JSONObject jsonObject = JSONUtil.parseObj(html); + Assert.assertEquals("Something must have been changed since you leave", jsonObject.getStr("name")); + } + + @Test + public void getStrTest2() { + String html = "{\"name\":\"Something\\u00a0must have been changed since you leave\"}"; + JSONObject jsonObject = JSONUtil.parseObj(html); + Assert.assertEquals("Something\\u00a0must\\u00a0have\\u00a0been\\u00a0changed\\u00a0since\\u00a0you\\u00a0leave", jsonObject.getStrEscaped("name")); + } + + @Test + public void parseFromXmlTest() { + String s = "640102197312070614640102197312070614Xaa1"; + JSONObject json = JSONUtil.parseFromXml(s); + Assert.assertEquals(640102197312070614L, json.get("sfzh")); + Assert.assertEquals("640102197312070614X", json.get("sfz")); + Assert.assertEquals("aa", json.get("name")); + Assert.assertEquals(1, json.get("gender")); + } +} diff --git a/hutool-json/src/test/java/cn/hutool/json/ParseBeanTest.java b/hutool-json/src/test/java/cn/hutool/json/ParseBeanTest.java new file mode 100644 index 000000000..3ee2d720f --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/ParseBeanTest.java @@ -0,0 +1,78 @@ +package cn.hutool.json; + +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.collection.CollUtil; + +/** + * 测试Bean中嵌套List等对象时是否完整转换
+ * 同时测试私有class是否可以有效实例化 + * + * @author looly + * + */ +public class ParseBeanTest { + + @Test + public void parseBeanTest() { + + C c1 = new C(); + c1.setTest("test1"); + C c2 = new C(); + c2.setTest("test2"); + + B b1 = new B(); + b1.setCs(CollUtil.newArrayList(c1, c2)); + B b2 = new B(); + b2.setCs(CollUtil.newArrayList(c1, c2)); + + A a = new A(); + a.setBs(CollUtil.newArrayList(b1, b2)); + + JSONObject json = JSONUtil.parseObj(a); + A a1 = JSONUtil.toBean(json, A.class); + Assert.assertEquals(json.toString(), JSONUtil.toJsonStr(a1)); + } + +} + +class A { + + private List bs; + + public List getBs() { + return bs; + } + + public void setBs(List bs) { + this.bs = bs; + } +} + +class B { + + private List cs; + + public List getCs() { + return cs; + } + + public void setCs(List cs) { + this.cs = cs; + } +} + +class C { + private String test; + + public String getTest() { + return test; + } + + public void setTest(String test) { + this.test = test; + } +} diff --git a/hutool-json/src/test/java/cn/hutool/json/issueIVMD5/BaseResult.java b/hutool-json/src/test/java/cn/hutool/json/issueIVMD5/BaseResult.java new file mode 100644 index 000000000..cdc9c4c50 --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/issueIVMD5/BaseResult.java @@ -0,0 +1,20 @@ +package cn.hutool.json.issueIVMD5; + +import java.util.List; + +import lombok.Data; + +@Data +public class BaseResult { + + public BaseResult() { + } + + private int result; + private List data; + private E data2; + private String nextDataUri; + private String message; + private int dataCount; + +} \ No newline at end of file diff --git a/hutool-json/src/test/java/cn/hutool/json/issueIVMD5/IssueIVMD5Test.java b/hutool-json/src/test/java/cn/hutool/json/issueIVMD5/IssueIVMD5Test.java new file mode 100644 index 000000000..2a9166712 --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/issueIVMD5/IssueIVMD5Test.java @@ -0,0 +1,42 @@ +package cn.hutool.json.issueIVMD5; + +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.lang.TypeReference; +import cn.hutool.json.JSONUtil; + +public class IssueIVMD5Test { + + /** + * 测试泛型对象中有泛型字段的转换成功与否 + */ + @Test + public void toBeanTest() { + String jsonStr = ResourceUtil.readUtf8Str("issueIVMD5.json"); + + TypeReference> typeReference = new TypeReference>() {}; + BaseResult bean = JSONUtil.toBean(jsonStr, typeReference.getType(), false); + + StudentInfo data2 = bean.getData2(); + Assert.assertEquals("B4DDF491FDF34074AE7A819E1341CB6C", data2.getAccountId()); + } + + /** + * 测试泛型对象中有包含泛型字段的类型的转换成功与否,比如List<T> list + */ + @Test + public void toBeanTest2() { + String jsonStr = ResourceUtil.readUtf8Str("issueIVMD5.json"); + + TypeReference> typeReference = new TypeReference>() {}; + BaseResult bean = JSONUtil.toBean(jsonStr, typeReference.getType(), false); + + List data = bean.getData(); + StudentInfo studentInfo = data.get(0); + Assert.assertEquals("B4DDF491FDF34074AE7A819E1341CB6C", studentInfo.getAccountId()); + } +} diff --git a/hutool-json/src/test/java/cn/hutool/json/issueIVMD5/StudentInfo.java b/hutool-json/src/test/java/cn/hutool/json/issueIVMD5/StudentInfo.java new file mode 100644 index 000000000..c7ba70f5b --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/issueIVMD5/StudentInfo.java @@ -0,0 +1,28 @@ +package cn.hutool.json.issueIVMD5; + +import lombok.Data; + +@Data +public class StudentInfo { + private String birthday; + private String linkPhone; + private String nation; + private String studentCode; + private String unitiveCode; + private String sex; + private String linkAddress; + private String identityCard; + private String accountId; + private String classId; + private String password; + private String modifyTime; + private String isDeleted; + private String postalcode; + private String background; + private String schoolId; + private String studentName; + private String sequenceIntId; + private String nativePlace; + private String id; + private String username; +} diff --git a/hutool-json/src/test/java/cn/hutool/json/test/bean/ADT.java b/hutool-json/src/test/java/cn/hutool/json/test/bean/ADT.java new file mode 100644 index 000000000..af708c872 --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/test/bean/ADT.java @@ -0,0 +1,16 @@ +package cn.hutool.json.test.bean; + +import java.util.List; + +public class ADT { + + private List BookingCode; + + public void setBookingCode(List BookingCode) { + this.BookingCode = BookingCode; + } + + public List getBookingCode() { + return BookingCode; + } +} diff --git a/hutool-json/src/test/java/cn/hutool/json/test/bean/Data.java b/hutool-json/src/test/java/cn/hutool/json/test/bean/Data.java new file mode 100644 index 000000000..535b7212c --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/test/bean/Data.java @@ -0,0 +1,14 @@ +package cn.hutool.json.test.bean; + +public class Data { + + private Price Price; + + public void setPrice(Price Price) { + this.Price = Price; + } + + public Price getPrice() { + return Price; + } +} diff --git a/hutool-json/src/test/java/cn/hutool/json/test/bean/Exam.java b/hutool-json/src/test/java/cn/hutool/json/test/bean/Exam.java new file mode 100644 index 000000000..b867f8ffb --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/test/bean/Exam.java @@ -0,0 +1,67 @@ +package cn.hutool.json.test.bean; + +import java.util.Arrays; + +public class Exam { + private String id; + private String examNumber; + private String isAnswer; + private Seq[] answerArray; + private String isRight; + private String isSubject; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getExamNumber() { + return examNumber; + } + + public void setExamNumber(String examNumber) { + this.examNumber = examNumber; + } + + public String getIsAnswer() { + return isAnswer; + } + + public void setIsAnswer(String isAnswer) { + this.isAnswer = isAnswer; + } + + public Seq[] getAnswerArray() { + return answerArray; + } + + public void setAnswerArray(Seq[] answerArray) { + this.answerArray = answerArray; + } + + public String getIsRight() { + return isRight; + } + + public void setIsRight(String isRight) { + this.isRight = isRight; + } + + public String getIsSubject() { + return isSubject; + } + + public void setIsSubject(String isSubject) { + this.isSubject = isSubject; + } + + @Override + public String toString() { + return "Exam [id=" + id + ", examNumber=" + examNumber + ", isAnswer=" + isAnswer + ", answerArray=" + Arrays.toString(answerArray) + ", isRight=" + isRight + ", isSubject=" + isSubject + "]"; + } + + +} diff --git a/hutool-json/src/test/java/cn/hutool/json/test/bean/ExamInfoDict.java b/hutool-json/src/test/java/cn/hutool/json/test/bean/ExamInfoDict.java new file mode 100644 index 000000000..172d79da7 --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/test/bean/ExamInfoDict.java @@ -0,0 +1,63 @@ +package cn.hutool.json.test.bean; + +import java.io.Serializable; +import java.util.Objects; + +/** + * + * @author 质量过关 + * + */ +public class ExamInfoDict implements Serializable { + private static final long serialVersionUID = 3640936499125004525L; + + // 主键 + private Integer id; // 可当作题号 + // 试题类型 客观题 0主观题 1 + private Integer examType; + // 试题是否作答 + private Integer answerIs; + + public Integer getId() { + return id; + } + public void setId(Integer id) { + this.id = id; + } + + public Integer getExamType() { + return examType; + } + public void setExamType(Integer examType) { + this.examType = examType; + } + + public Integer getAnswerIs() { + return answerIs; + } + public void setAnswerIs(Integer answerIs) { + this.answerIs = answerIs; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ExamInfoDict that = (ExamInfoDict) o; + return Objects.equals(id, that.id) && Objects.equals(examType, that.examType) && Objects.equals(answerIs, that.answerIs); + } + + @Override + public int hashCode() { + return Objects.hash(id, examType, answerIs); + } + + @Override + public String toString() { + return "ExamInfoDict{" + "id=" + id + ", examType=" + examType + ", answerIs=" + answerIs + '}'; + } +} diff --git a/hutool-json/src/test/java/cn/hutool/json/test/bean/JSONBean.java b/hutool-json/src/test/java/cn/hutool/json/test/bean/JSONBean.java new file mode 100644 index 000000000..389c3ac26 --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/test/bean/JSONBean.java @@ -0,0 +1,22 @@ +package cn.hutool.json.test.bean; + +import cn.hutool.json.JSONObject; + +public class JSONBean { + + private int code; + private JSONObject data; + + public int getCode() { + return code; + } + public void setCode(int code) { + this.code = code; + } + public JSONObject getData() { + return data; + } + public void setData(JSONObject data) { + this.data = data; + } +} diff --git a/hutool-json/src/test/java/cn/hutool/json/test/bean/JsonNode.java b/hutool-json/src/test/java/cn/hutool/json/test/bean/JsonNode.java new file mode 100644 index 000000000..02275009e --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/test/bean/JsonNode.java @@ -0,0 +1,49 @@ +package cn.hutool.json.test.bean; + +import java.io.Serializable; + +public class JsonNode implements Serializable { + private static final long serialVersionUID = -2280206942803550272L; + + private Long id; + private Integer parentId; + private String name; + + public JsonNode() { + } + + public JsonNode(Long id, Integer parentId, String name) { + this.id = id; + this.parentId = parentId; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Integer getParentId() { + return parentId; + } + + public void setParentId(Integer parentId) { + this.parentId = parentId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "JsonNode{" + "id=" + id + ", parentId=" + parentId + ", name='" + name + '\'' + '}'; + } +} \ No newline at end of file diff --git a/hutool-json/src/test/java/cn/hutool/json/test/bean/JsonRootBean.java b/hutool-json/src/test/java/cn/hutool/json/test/bean/JsonRootBean.java new file mode 100644 index 000000000..fdb2c963c --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/test/bean/JsonRootBean.java @@ -0,0 +1,61 @@ +package cn.hutool.json.test.bean; + +import java.util.List; + +public class JsonRootBean { + + private int statusCode; + private String message; + private int skip; + private int limit; + private int total; + private List data; + + public void setStatusCode(int statusCode) { + this.statusCode = statusCode; + } + + public int getStatusCode() { + return statusCode; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + + public void setSkip(int skip) { + this.skip = skip; + } + + public int getSkip() { + return skip; + } + + public void setLimit(int limit) { + this.limit = limit; + } + + public int getLimit() { + return limit; + } + + public void setTotal(int total) { + this.total = total; + } + + public int getTotal() { + return total; + } + + public void setData(List data) { + this.data = data; + } + + public List getData() { + return data; + } +} diff --git a/hutool-json/src/test/java/cn/hutool/json/test/bean/KeyBean.java b/hutool-json/src/test/java/cn/hutool/json/test/bean/KeyBean.java new file mode 100644 index 000000000..663d12e4e --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/test/bean/KeyBean.java @@ -0,0 +1,24 @@ +package cn.hutool.json.test.bean; + +public class KeyBean{ + private String akey; + private String bkey; + + public String getAkey() { + return akey; + } + public void setAkey(String akey) { + this.akey = akey; + } + public String getBkey() { + return bkey; + } + public void setBkey(String bkey) { + this.bkey = bkey; + } + + @Override + public String toString() { + return "KeyBean [akey=" + akey + ", bkey=" + bkey + "]"; + } +} diff --git a/hutool-json/src/test/java/cn/hutool/json/test/bean/PerfectEvaluationProductResVo.java b/hutool-json/src/test/java/cn/hutool/json/test/bean/PerfectEvaluationProductResVo.java new file mode 100644 index 000000000..ce0487efb --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/test/bean/PerfectEvaluationProductResVo.java @@ -0,0 +1,965 @@ +package cn.hutool.json.test.bean; + +import java.util.HashMap; +import java.util.Map; + +/** +* @author wangyan E-mail:wangyan@pospt.cn +* @version 创建时间:2017年9月13日 下午5:16:32 +* 类说明 +*/ +public class PerfectEvaluationProductResVo extends ProductResBase{ + + public static final Map KEY_TO_KEY = new HashMap(){ + private static final long serialVersionUID = 1L; + { + put("HA001","CDCA005"); + put("HA002","CDCA002"); + put("HA003","CDCA004"); + put("HA004","CSSP002"); + put("HB001","CSWC001"); + put("HB002","CSRL001"); + put("HB003","CSRL003"); + put("HC005","CDTB016"); + put("HC006","CDTB020"); + put("HC007","CDTB021"); + put("HC008","CDTB022"); + put("HC009","CDTB023"); + put("HC010","CDTB024"); + put("HC011","CDTB001"); + put("HC012","CDTB005"); + put("HC013","CDTB006"); + put("HC014","CDTB007"); + put("HC015","CDTB008"); + put("HC016","CDTB009"); + put("HC001","CDTB018"); + put("HC002","CDTB003"); + put("HC003","CDTB063"); + put("HC004","CDTB060"); + put("HD001","CDMC005"); + put("HD007","CDMC082"); + put("HD010","CDMC164"); + put("HD016","CSSP003"); + put("HD017","CSSS003"); + put("HE001","CDTT028"); + put("HE002","CDTT029"); + put("HE003","CDTT030"); + put("HE004","CDTT031"); + put("HE005","CDTT032"); + put("HE006","CDTT033"); + put("HE007","CDTT015"); + put("HE008","CDTT016"); + put("HE009","CDTT017"); + put("HE010","CDTT018"); + put("HE011","CDTT019"); + put("HE012","CDTT020"); + put("HE013","CDTT055"); + put("HE014","CDTT056"); + put("HE015","CDTT057"); + put("HE016","CDTT058"); + put("HE017","CDTT059"); + put("HE018","CDTT060"); + put("HE019","CDTT043"); + put("HE020","CDTT044"); + put("HE021","CDTT045"); + put("HE022","CDTT046"); + put("HE023","CDTT047"); + put("HE024","CDTT048"); + put("HE025","CDTT080"); + put("HE026","CDTT081"); + put("HE027","CDTT082"); + put("HE028","CDTT083"); + put("HE029","CDTT084"); + put("HE030","CDTT085"); + put("HE031","CDTT067"); + put("HE032","CDTT068"); + put("HE033","CDTT069"); + put("HE034","CDTT070"); + put("HE035","CDTT071"); + put("HE036","CDTT072"); + put("HF001","CDTB272"); + put("HF002","CDTB273"); + put("HF003","CDTB277"); + put("HF004","CDTB278"); + put("HF005","CDTB282"); + put("HF006","CDTB283"); + put("HF007","CDTC058"); + put("HF008","CDTC059"); + put("HF009","CDTC060"); + put("HF010","CDTC014"); + put("HG001","CDMC294"); + put("HG002","CDMC293"); + put("HG003","CDMC301"); + put("HG004","CDMC300"); + put("HG005","CDMC308"); + put("HG006","CDMC307"); + put("HG007","CDMC315"); + put("HG008","CDMC314"); + put("HG009","CDMC322"); + put("HG010","CDMC321"); + put("HG011","CDMC287"); + put("HH001","CDTT001"); + put("HH002","CDTT002"); + put("HH003","CDTT003"); + put("HH004","CDTT004"); + put("HH005","CDTT005"); + put("HH006","CDTT006"); + put("HH007","CDTT007"); + put("HH008","CDTT008"); + put("HH009","CDTT009"); + put("HH010","CDTT010"); + put("HH011","CDTT011"); + put("HH012","CDTT012"); + put("HH013","CDTT013"); + put("HH014","CDTT014"); + } + }; + + private static final long serialVersionUID = 1L; + private String XT_NO; + private String CARD_HOLDER; + private String CARD_NO; + private String HA001; + private String HA002; + private String HA003; + private String HA004; + private String HB001; + private String HB002; + private String HB003; + private String HC005; + private String HC006; + private String HC007; + private String HC008; + private String HC009; + private String HC010; + private String HC011; + private String HC012; + private String HC013; + private String HC014; + private String HC015; + private String HC016; + private String HC001; + private String HC002; + private String HC003; + private String HC004; + private String HD001; + private String HD002; + private String HD004; + private String HD005; + private String HD007; + private String HD008; + private String HD010; + private String HD011; + private String HD013; + private String HD014; + private String HD016; + private String HD017; + private String HE001; + private String HE002; + private String HE003; + private String HE004; + private String HE005; + private String HE006; + private String HE007; + private String HE008; + private String HE009; + private String HE010; + private String HE011; + private String HE012; + private String HE013; + private String HE014; + private String HE015; + private String HE016; + private String HE017; + private String HE018; + private String HE019; + private String HE020; + private String HE021; + private String HE022; + private String HE023; + private String HE024; + private String HE025; + private String HE026; + private String HE027; + private String HE028; + private String HE029; + private String HE030; + private String HE031; + private String HE032; + private String HE033; + private String HE034; + private String HE035; + private String HE036; + private String HF001; + private String HF002; + private String HF003; + private String HF004; + private String HF005; + private String HF006; + private String HF007; + private String HF008; + private String HF009; + private String HF010; + private String HG001; + private String HG002; + private String HG003; + private String HG004; + private String HG005; + private String HG006; + private String HG007; + private String HG008; + private String HG009; + private String HG010; + private String HG011; + private String HG012; + private String HG013; + private String HG014; + private String HG015; + private String HG016; + private String HH001; + private String HH002; + private String HH003; + private String HH004; + private String HH005; + private String HH006; + private String HH007; + private String HH008; + private String HH009; + private String HH010; + private String HH011; + private String HH012; + private String HH013; + private String HH014; + private String XT_MONTH01; + private String XT_MONTH02; + private String XT_MONTH03; + private String XT_MONTH04; + private String XT_MONTH05; + private String XT_MONTH06; + + + public String getHA001() { + return HA001; + } + public String getHA002() { + return HA002; + } + public String getHA003() { + return HA003; + } + public String getHA004() { + return HA004; + } + public String getHB001() { + return HB001; + } + public String getHB002() { + return HB002; + } + public String getHB003() { + return HB003; + } + public String getHC005() { + return HC005; + } + public String getHC006() { + return HC006; + } + public String getHC007() { + return HC007; + } + public String getHC008() { + return HC008; + } + public String getHC009() { + return HC009; + } + public String getHC010() { + return HC010; + } + public String getHC011() { + return HC011; + } + public String getHC012() { + return HC012; + } + public String getHC013() { + return HC013; + } + public String getHC014() { + return HC014; + } + public String getHC015() { + return HC015; + } + public String getHC016() { + return HC016; + } + public String getHC001() { + return HC001; + } + public String getHC002() { + return HC002; + } + public String getHC003() { + return HC003; + } + public String getHC004() { + return HC004; + } + public String getHD001() { + return HD001; + } + public String getHD002() { + return HD002; + } + + public String getHD004() { + return HD004; + } + public String getHD005() { + return HD005; + } + + public String getHD007() { + return HD007; + } + public String getHD008() { + return HD008; + } + + public String getHD010() { + return HD010; + } + public String getHD011() { + return HD011; + } + public String getHD013() { + return HD013; + } + public String getHD014() { + return HD014; + } + public String getHD016() { + return HD016; + } + public String getHD017() { + return HD017; + } + public String getHE001() { + return HE001; + } + public String getHE002() { + return HE002; + } + public String getHE003() { + return HE003; + } + public String getHE004() { + return HE004; + } + public String getHE005() { + return HE005; + } + public String getHE006() { + return HE006; + } + public String getHE007() { + return HE007; + } + public String getHE008() { + return HE008; + } + public String getHE009() { + return HE009; + } + public String getHE010() { + return HE010; + } + public String getHE011() { + return HE011; + } + public String getHE012() { + return HE012; + } + public String getHE013() { + return HE013; + } + public String getHE014() { + return HE014; + } + public String getHE015() { + return HE015; + } + public String getHE016() { + return HE016; + } + public String getHE017() { + return HE017; + } + public String getHE018() { + return HE018; + } + public String getHE019() { + return HE019; + } + public String getHE020() { + return HE020; + } + public String getHE021() { + return HE021; + } + public String getHE022() { + return HE022; + } + public String getHE023() { + return HE023; + } + public String getHE024() { + return HE024; + } + public String getHE025() { + return HE025; + } + public String getHE026() { + return HE026; + } + public String getHE027() { + return HE027; + } + public String getHE028() { + return HE028; + } + public String getHE029() { + return HE029; + } + public String getHE030() { + return HE030; + } + public String getHE031() { + return HE031; + } + public String getHE032() { + return HE032; + } + public String getHE033() { + return HE033; + } + public String getHE034() { + return HE034; + } + public String getHE035() { + return HE035; + } + public String getHE036() { + return HE036; + } + public String getHF001() { + return HF001; + } + public String getHF002() { + return HF002; + } + public String getHF003() { + return HF003; + } + public String getHF004() { + return HF004; + } + public String getHF005() { + return HF005; + } + public String getHF006() { + return HF006; + } + public String getHF007() { + return HF007; + } + public String getHF008() { + return HF008; + } + public String getHF009() { + return HF009; + } + public String getHF010() { + return HF010; + } + public String getHG001() { + return HG001; + } + public String getHG002() { + return HG002; + } + public String getHG003() { + return HG003; + } + public String getHG004() { + return HG004; + } + public String getHG005() { + return HG005; + } + public String getHG006() { + return HG006; + } + public String getHG007() { + return HG007; + } + public String getHG008() { + return HG008; + } + public String getHG009() { + return HG009; + } + public String getHG010() { + return HG010; + } + public String getHG011() { + return HG011; + } + public String getHG012() { + return HG012; + } + public String getHG013() { + return HG013; + } + public String getHG014() { + return HG014; + } + public String getHG015() { + return HG015; + } + public String getHG016() { + return HG016; + } + public String getHH001() { + return HH001; + } + public String getHH002() { + return HH002; + } + public String getHH003() { + return HH003; + } + public String getHH004() { + return HH004; + } + public String getHH005() { + return HH005; + } + public String getHH006() { + return HH006; + } + public String getHH007() { + return HH007; + } + public String getHH008() { + return HH008; + } + public String getHH009() { + return HH009; + } + public String getHH010() { + return HH010; + } + public String getHH011() { + return HH011; + } + public String getHH012() { + return HH012; + } + public String getHH013() { + return HH013; + } + public String getHH014() { + return HH014; + } + public void setHA001(String hA001) { + HA001 = hA001; + } + public void setHA002(String hA002) { + HA002 = hA002; + } + public void setHA003(String hA003) { + HA003 = hA003; + } + public void setHA004(String hA004) { + HA004 = hA004; + } + public void setHB001(String hB001) { + HB001 = hB001; + } + public void setHB002(String hB002) { + HB002 = hB002; + } + public void setHB003(String hB003) { + HB003 = hB003; + } + public void setHC005(String hC005) { + HC005 = hC005; + } + public void setHC006(String hC006) { + HC006 = hC006; + } + public void setHC007(String hC007) { + HC007 = hC007; + } + public void setHC008(String hC008) { + HC008 = hC008; + } + public void setHC009(String hC009) { + HC009 = hC009; + } + public void setHC010(String hC010) { + HC010 = hC010; + } + public void setHC011(String hC011) { + HC011 = hC011; + } + public void setHC012(String hC012) { + HC012 = hC012; + } + public void setHC013(String hC013) { + HC013 = hC013; + } + public void setHC014(String hC014) { + HC014 = hC014; + } + public void setHC015(String hC015) { + HC015 = hC015; + } + public void setHC016(String hC016) { + HC016 = hC016; + } + public void setHC001(String hC001) { + HC001 = hC001; + } + public void setHC002(String hC002) { + HC002 = hC002; + } + public void setHC003(String hC003) { + HC003 = hC003; + } + public void setHC004(String hC004) { + HC004 = hC004; + } + public void setHD001(String hD001) { + HD001 = hD001; + } + public void setHD002(String hD002) { + HD002 = hD002; + } + public void setHD004(String hD004) { + HD004 = hD004; + } + public void setHD005(String hD005) { + HD005 = hD005; + } + public void setHD007(String hD007) { + HD007 = hD007; + } + public void setHD008(String hD008) { + HD008 = hD008; + } + public void setHD010(String hD010) { + HD010 = hD010; + } + public void setHD011(String hD011) { + HD011 = hD011; + } + public void setHD013(String hD013) { + HD013 = hD013; + } + public void setHD014(String hD014) { + HD014 = hD014; + } + public void setHD016(String hD016) { + HD016 = hD016; + } + public void setHD017(String hD017) { + HD017 = hD017; + } + public void setHE001(String hE001) { + HE001 = hE001; + } + public void setHE002(String hE002) { + HE002 = hE002; + } + public void setHE003(String hE003) { + HE003 = hE003; + } + public void setHE004(String hE004) { + HE004 = hE004; + } + public void setHE005(String hE005) { + HE005 = hE005; + } + public void setHE006(String hE006) { + HE006 = hE006; + } + public void setHE007(String hE007) { + HE007 = hE007; + } + public void setHE008(String hE008) { + HE008 = hE008; + } + public void setHE009(String hE009) { + HE009 = hE009; + } + public void setHE010(String hE010) { + HE010 = hE010; + } + public void setHE011(String hE011) { + HE011 = hE011; + } + public void setHE012(String hE012) { + HE012 = hE012; + } + public void setHE013(String hE013) { + HE013 = hE013; + } + public void setHE014(String hE014) { + HE014 = hE014; + } + public void setHE015(String hE015) { + HE015 = hE015; + } + public void setHE016(String hE016) { + HE016 = hE016; + } + public void setHE017(String hE017) { + HE017 = hE017; + } + public void setHE018(String hE018) { + HE018 = hE018; + } + public void setHE019(String hE019) { + HE019 = hE019; + } + public void setHE020(String hE020) { + HE020 = hE020; + } + public void setHE021(String hE021) { + HE021 = hE021; + } + public void setHE022(String hE022) { + HE022 = hE022; + } + public void setHE023(String hE023) { + HE023 = hE023; + } + public void setHE024(String hE024) { + HE024 = hE024; + } + public void setHE025(String hE025) { + HE025 = hE025; + } + public void setHE026(String hE026) { + HE026 = hE026; + } + public void setHE027(String hE027) { + HE027 = hE027; + } + public void setHE028(String hE028) { + HE028 = hE028; + } + public void setHE029(String hE029) { + HE029 = hE029; + } + public void setHE030(String hE030) { + HE030 = hE030; + } + public void setHE031(String hE031) { + HE031 = hE031; + } + public void setHE032(String hE032) { + HE032 = hE032; + } + public void setHE033(String hE033) { + HE033 = hE033; + } + public void setHE034(String hE034) { + HE034 = hE034; + } + public void setHE035(String hE035) { + HE035 = hE035; + } + public void setHE036(String hE036) { + HE036 = hE036; + } + public void setHF001(String hF001) { + HF001 = hF001; + } + public void setHF002(String hF002) { + HF002 = hF002; + } + public void setHF003(String hF003) { + HF003 = hF003; + } + public void setHF004(String hF004) { + HF004 = hF004; + } + public void setHF005(String hF005) { + HF005 = hF005; + } + public void setHF006(String hF006) { + HF006 = hF006; + } + public void setHF007(String hF007) { + HF007 = hF007; + } + public void setHF008(String hF008) { + HF008 = hF008; + } + public void setHF009(String hF009) { + HF009 = hF009; + } + public void setHF010(String hF010) { + HF010 = hF010; + } + public void setHG001(String hG001) { + HG001 = hG001; + } + public void setHG002(String hG002) { + HG002 = hG002; + } + public void setHG003(String hG003) { + HG003 = hG003; + } + public void setHG004(String hG004) { + HG004 = hG004; + } + public void setHG005(String hG005) { + HG005 = hG005; + } + public void setHG006(String hG006) { + HG006 = hG006; + } + public void setHG007(String hG007) { + HG007 = hG007; + } + public void setHG008(String hG008) { + HG008 = hG008; + } + public void setHG009(String hG009) { + HG009 = hG009; + } + public void setHG010(String hG010) { + HG010 = hG010; + } + public void setHG011(String hG011) { + HG011 = hG011; + } + public void setHG012(String hG012) { + HG012 = hG012; + } + public void setHG013(String hG013) { + HG013 = hG013; + } + public void setHG014(String hG014) { + HG014 = hG014; + } + public void setHG015(String hG015) { + HG015 = hG015; + } + public void setHG016(String hG016) { + HG016 = hG016; + } + public void setHH001(String hH001) { + HH001 = hH001; + } + public void setHH002(String hH002) { + HH002 = hH002; + } + public void setHH003(String hH003) { + HH003 = hH003; + } + public void setHH004(String hH004) { + HH004 = hH004; + } + public void setHH005(String hH005) { + HH005 = hH005; + } + public void setHH006(String hH006) { + HH006 = hH006; + } + public void setHH007(String hH007) { + HH007 = hH007; + } + public void setHH008(String hH008) { + HH008 = hH008; + } + public void setHH009(String hH009) { + HH009 = hH009; + } + public void setHH010(String hH010) { + HH010 = hH010; + } + public void setHH011(String hH011) { + HH011 = hH011; + } + public void setHH012(String hH012) { + HH012 = hH012; + } + public void setHH013(String hH013) { + HH013 = hH013; + } + public void setHH014(String hH014) { + HH014 = hH014; + } + public String getXT_NO() { + return XT_NO; + } + public void setXT_NO(String xT_NO) { + XT_NO = xT_NO; + } + public String getCARD_HOLDER() { + return CARD_HOLDER; + } + public void setCARD_HOLDER(String cARD_HOLDER) { + CARD_HOLDER = cARD_HOLDER; + } + public String getCARD_NO() { + return CARD_NO; + } + public String getXT_MONTH01() { + return XT_MONTH01; + } + public String getXT_MONTH02() { + return XT_MONTH02; + } + public String getXT_MONTH03() { + return XT_MONTH03; + } + public String getXT_MONTH04() { + return XT_MONTH04; + } + public String getXT_MONTH05() { + return XT_MONTH05; + } + public String getXT_MONTH06() { + return XT_MONTH06; + } + public void setCARD_NO(String cARD_NO) { + CARD_NO = cARD_NO; + } + public void setXT_MONTH01(String xT_MONTH01) { + XT_MONTH01 = xT_MONTH01; + } + public void setXT_MONTH02(String xT_MONTH02) { + XT_MONTH02 = xT_MONTH02; + } + public void setXT_MONTH03(String xT_MONTH03) { + XT_MONTH03 = xT_MONTH03; + } + public void setXT_MONTH04(String xT_MONTH04) { + XT_MONTH04 = xT_MONTH04; + } + public void setXT_MONTH05(String xT_MONTH05) { + XT_MONTH05 = xT_MONTH05; + } + public void setXT_MONTH06(String xT_MONTH06) { + XT_MONTH06 = xT_MONTH06; + } + +} diff --git a/hutool-json/src/test/java/cn/hutool/json/test/bean/Price.java b/hutool-json/src/test/java/cn/hutool/json/test/bean/Price.java new file mode 100644 index 000000000..f9c5f4c24 --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/test/bean/Price.java @@ -0,0 +1,16 @@ +package cn.hutool.json.test.bean; + +import java.util.List; + +public class Price { + + private List> ADT; + + public void setADT(List> ADT) { + this.ADT = ADT; + } + + public List> getADT() { + return ADT; + } +} \ No newline at end of file diff --git a/hutool-json/src/test/java/cn/hutool/json/test/bean/ProductResBase.java b/hutool-json/src/test/java/cn/hutool/json/test/bean/ProductResBase.java new file mode 100644 index 000000000..86772dbfc --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/test/bean/ProductResBase.java @@ -0,0 +1,71 @@ +package cn.hutool.json.test.bean; + +import java.io.Serializable; + +/** + * @author wangyan E-mail:wangyan@pospt.cn + * @version 创建时间:2017年9月11日 上午9:33:01 类说明 + */ +public class ProductResBase implements Serializable { + + private static final long serialVersionUID = -6708040074002451511L; + /** + * 请求结果成功0 + */ + public static final int REQUEST_RESULT_SUCCESS = 0; + /** + * 请求结果失败 1 + */ + public static final int REQUEST_RESULT_FIAL = 1; + /** + * 成功code + */ + public static final String REQUEST_CODE_SUCCESS = "0000"; + /** + * 结果 成功0 失败1 + */ + private int resResult = 0; + private String resCode = "0000"; + private String resMsg = "success"; + + /** + * 成本总计 + */ + private Integer costTotal; + + public Integer getCostTotal() { + return costTotal; + } + + public ProductResBase setCostTotal(Integer costTotal) { + this.costTotal = costTotal; + return this; + } + + public int getResResult() { + return resResult; + } + + public String getResCode() { + return resCode; + } + + public String getResMsg() { + return resMsg; + } + + public ProductResBase setResResult(int resResult) { + this.resResult = resResult; + return this; + } + + public ProductResBase setResCode(String resCode) { + this.resCode = resCode; + return this; + } + + public ProductResBase setResMsg(String resMsg) { + this.resMsg = resMsg; + return this; + } +} diff --git a/hutool-json/src/test/java/cn/hutool/json/test/bean/ResultDto.java b/hutool-json/src/test/java/cn/hutool/json/test/bean/ResultDto.java new file mode 100644 index 000000000..8cc3662ae --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/test/bean/ResultDto.java @@ -0,0 +1,156 @@ +package cn.hutool.json.test.bean; + +import java.io.Serializable; + +public class ResultDto implements Serializable { + private static final long serialVersionUID = -1417999729205654379L; + + /** + * 成功码. + */ + public static final int SUCCESS_CODE = 200; + + /** + * 成功信息. + */ + public static final String SUCCESS_MESSAGE = "操作成功"; + + /** + * 错误码. + */ + public static final int ERROR_CODE = 500; + + /** + * 错误信息. + */ + public static final String ERROR_MESSAGE = "内部异常"; + + /** + * 错误码:参数非法 + */ + public static final int ILLEGAL_ARGUMENT_CODE_ = 100; + + /** + * 错误信息:参数非法 + */ + public static final String ILLEGAL_ARGUMENT_MESSAGE = "参数非法"; + + /** + * 编号. + */ + private int code; + + /** + * 信息. + */ + private String message; + + /** + * 结果数据 + */ + private T result; + + /** + * Instantiates a new wrapper. default code=200 + */ + public ResultDto() { + this(SUCCESS_CODE, SUCCESS_MESSAGE); + } + + /** + * Instantiates a new wrapper. + * + * @param code the code + * @param message the message + */ + public ResultDto(int code, String message) { + this(code, message, null); + } + + /** + * Instantiates a new wrapper. + * + * @param code the code + * @param message the message + * @param result the result + */ + ResultDto(int code, String message, T result) { + super(); + this.code(code).message(message).result(result); + } + + /** + * Sets the 编号 , 返回自身的引用. + * + * @param code the new 编号 + * @return the wrapper + */ + private ResultDto code(int code) { + this.setCode(code); + return this; + } + + /** + * Sets the 信息 , 返回自身的引用. + * + * @param message the new 信息 + * @return the wrapper + */ + private ResultDto message(String message) { + this.setMessage(message); + return this; + } + + /** + * Sets the 结果数据 , 返回自身的引用. + * + * @param result the new 结果数据 + * @return the wrapper + */ + public ResultDto result(T result) { + this.setResult(result); + return this; + } + + /** + * 判断是否成功: 依据 ResultDto.SUCCESS_CODE == this.code + * + * @return code =200,true;否则 false. + */ + public boolean success() { + return ResultDto.SUCCESS_CODE == this.code; + } + + /** + * 判断是否成功: 依据 ResultDto.SUCCESS_CODE != this.code + * + * @return code !=200,true;否则 false. + */ + public boolean error() { + return !success(); + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public T getResult() { + return result; + } + + public void setResult(T result) { + this.result = result; + } +} diff --git a/hutool-json/src/test/java/cn/hutool/json/test/bean/Seq.java b/hutool-json/src/test/java/cn/hutool/json/test/bean/Seq.java new file mode 100644 index 000000000..253ee53b3 --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/test/bean/Seq.java @@ -0,0 +1,25 @@ +package cn.hutool.json.test.bean; + +public class Seq { + private String seq; + + public Seq() { + } + + public Seq(String seq) { + this.seq = seq; + } + + public String getSeq() { + return seq; + } + + public void setSeq(String seq) { + this.seq = seq; + } + + @Override + public String toString() { + return "Seq [seq=" + seq + "]"; + } +} diff --git a/hutool-json/src/test/java/cn/hutool/json/test/bean/TokenAuthResponse.java b/hutool-json/src/test/java/cn/hutool/json/test/bean/TokenAuthResponse.java new file mode 100644 index 000000000..1433381f7 --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/test/bean/TokenAuthResponse.java @@ -0,0 +1,22 @@ +package cn.hutool.json.test.bean; + +public class TokenAuthResponse { + private String token; + private String userId; + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } +} diff --git a/hutool-json/src/test/java/cn/hutool/json/test/bean/TokenAuthWarp.java b/hutool-json/src/test/java/cn/hutool/json/test/bean/TokenAuthWarp.java new file mode 100644 index 000000000..f357835c7 --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/test/bean/TokenAuthWarp.java @@ -0,0 +1,24 @@ +package cn.hutool.json.test.bean; + +public class TokenAuthWarp extends UUMap { + private static final long serialVersionUID = 1L; + + private String targetUrl; + private String success; + + public String getTargetUrl() { + return targetUrl; + } + + public void setTargetUrl(String targetUrl) { + this.targetUrl = targetUrl; + } + + public String getSuccess() { + return success; + } + + public void setSuccess(String success) { + this.success = success; + } +} diff --git a/hutool-json/src/test/java/cn/hutool/json/test/bean/TokenAuthWarp2.java b/hutool-json/src/test/java/cn/hutool/json/test/bean/TokenAuthWarp2.java new file mode 100644 index 000000000..bc17f11aa --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/test/bean/TokenAuthWarp2.java @@ -0,0 +1,9 @@ +package cn.hutool.json.test.bean; + +public class TokenAuthWarp2 extends TokenAuthWarp { + + /** + * + */ + private static final long serialVersionUID = 1L; +} diff --git a/hutool-json/src/test/java/cn/hutool/json/test/bean/UUMap.java b/hutool-json/src/test/java/cn/hutool/json/test/bean/UUMap.java new file mode 100644 index 000000000..6bfa528ed --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/test/bean/UUMap.java @@ -0,0 +1,21 @@ +package cn.hutool.json.test.bean; + +import java.io.Serializable; + +public class UUMap implements Serializable{ + private static final long serialVersionUID = 1L; + + private T result; + + public T getResult() { + return result; + } + + public void setResult(T result) { + this.result = result; + } + + public static long getSerialversionuid() { + return serialVersionUID; + } +} diff --git a/hutool-json/src/test/java/cn/hutool/json/test/bean/UserA.java b/hutool-json/src/test/java/cn/hutool/json/test/bean/UserA.java new file mode 100644 index 000000000..fa8b5e626 --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/test/bean/UserA.java @@ -0,0 +1,40 @@ +package cn.hutool.json.test.bean; + +import java.util.Date; +import java.util.List; + +public class UserA { + private String name; + private String a; + private Date date; + private List sqs; + + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + public String getA() { + return a; + } + public void setA(String a) { + this.a = a; + } + public Date getDate() { + return date; + } + public void setDate(Date date) { + this.date = date; + } + public List getSqs() { + return sqs; + } + public void setSqs(List sqs) { + this.sqs = sqs; + } + @Override + public String toString() { + return "UserA [name=" + name + ", a=" + a + ", date=" + date + ", sqs=" + sqs + "]"; + } +} diff --git a/hutool-json/src/test/java/cn/hutool/json/test/bean/UserB.java b/hutool-json/src/test/java/cn/hutool/json/test/bean/UserB.java new file mode 100644 index 000000000..13b734ac4 --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/test/bean/UserB.java @@ -0,0 +1,28 @@ +package cn.hutool.json.test.bean; + +import java.util.Date; + +public class UserB { + private String name; + private String b; + private Date date; + + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + public String getB() { + return b; + } + public void setB(String a) { + this.b = a; + } + public Date getDate() { + return date; + } + public void setDate(Date date) { + this.date = date; + } +} diff --git a/hutool-json/src/test/java/cn/hutool/json/test/bean/UserC.java b/hutool-json/src/test/java/cn/hutool/json/test/bean/UserC.java new file mode 100644 index 000000000..9c0753e76 --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/test/bean/UserC.java @@ -0,0 +1,31 @@ +package cn.hutool.json.test.bean; + +public class UserC { + private Integer id; + private String name; + private String prop; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getProp() { + return prop; + } + + public void setProp(String prop) { + this.prop = prop; + } +} diff --git a/hutool-json/src/test/java/cn/hutool/json/test/bean/UserInfoDict.java b/hutool-json/src/test/java/cn/hutool/json/test/bean/UserInfoDict.java new file mode 100644 index 000000000..5e7c2747a --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/test/bean/UserInfoDict.java @@ -0,0 +1,79 @@ +package cn.hutool.json.test.bean; + +import java.io.Serializable; +import java.util.List; +import java.util.Objects; + +/** + * 用户信息 + * @author 质量过关 + * + */ +public class UserInfoDict implements Serializable { + private static final long serialVersionUID = -936213991463284306L; + // 用户Id + private Integer id; + // 要展示的名字 + private String realName; + // 头像地址 + private String photoPath; + private List examInfoDict; + private UserInfoRedundCount userInfoRedundCount; + + public Integer getId() { + return id; + } + public void setId(Integer id) { + this.id = id; + } + + public String getRealName() { + return realName; + } + public void setRealName(String realName) { + this.realName = realName; + } + + public String getPhotoPath() { + return photoPath; + } + public void setPhotoPath(String photoPath) { + this.photoPath = photoPath; + } + + public List getExamInfoDict() { + return examInfoDict; + } + public void setExamInfoDict(List examInfoDict) { + this.examInfoDict = examInfoDict; + } + + public UserInfoRedundCount getUserInfoRedundCount() { + return userInfoRedundCount; + } + public void setUserInfoRedundCount(UserInfoRedundCount userInfoRedundCount) { + this.userInfoRedundCount = userInfoRedundCount; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + UserInfoDict that = (UserInfoDict) o; + return Objects.equals(id, that.id) && Objects.equals(realName, that.realName) && Objects.equals(photoPath, that.photoPath) && Objects.equals(examInfoDict, that.examInfoDict); + } + + @Override + public int hashCode() { + return Objects.hash(id, realName, photoPath, examInfoDict); + } + + @Override + public String toString() { + return "UserInfoDict [id=" + id + ", realName=" + realName + ", photoPath=" + photoPath + ", examInfoDict=" + examInfoDict + ", userInfoRedundCount=" + userInfoRedundCount + "]"; + } +} diff --git a/hutool-json/src/test/java/cn/hutool/json/test/bean/UserInfoRedundCount.java b/hutool-json/src/test/java/cn/hutool/json/test/bean/UserInfoRedundCount.java new file mode 100644 index 000000000..74556c058 --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/test/bean/UserInfoRedundCount.java @@ -0,0 +1,38 @@ +package cn.hutool.json.test.bean; + +import java.io.Serializable; + +public class UserInfoRedundCount implements Serializable { + + private static final long serialVersionUID = -8397291070139255181L; + private String finishedRatio; // 完成率 + + private Integer ownershipExamCount; // 自己有多少道题 + + private Integer answeredExamCount; // 当前回答了多少道题 + + public Integer getOwnershipExamCount() { + return ownershipExamCount; + } + + public void setOwnershipExamCount(Integer ownershipExamCount) { + this.ownershipExamCount = ownershipExamCount; + } + + public Integer getAnsweredExamCount() { + return answeredExamCount; + } + + public void setAnsweredExamCount(Integer answeredExamCount) { + this.answeredExamCount = answeredExamCount; + } + + public String getFinishedRatio() { + return finishedRatio; + } + + public void setFinishedRatio(String finishedRatio) { + this.finishedRatio = finishedRatio; + } + +} diff --git a/hutool-json/src/test/java/cn/hutool/json/test/bean/UserWithMap.java b/hutool-json/src/test/java/cn/hutool/json/test/bean/UserWithMap.java new file mode 100644 index 000000000..aa5965b6c --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/test/bean/UserWithMap.java @@ -0,0 +1,15 @@ +package cn.hutool.json.test.bean; + +import java.util.Map; + +public class UserWithMap { + private Map data; + + public Map getData() { + return data; + } + + public void setData(Map data) { + this.data = data; + } +} diff --git a/hutool-json/src/test/java/cn/hutool/json/test/bean/report/CaseReport.java b/hutool-json/src/test/java/cn/hutool/json/test/bean/report/CaseReport.java new file mode 100644 index 000000000..85dd44d24 --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/test/bean/report/CaseReport.java @@ -0,0 +1,31 @@ +package cn.hutool.json.test.bean.report; + +import java.util.ArrayList; +import java.util.List; + +/** + * 测试用例报告 + * @author xuwangcheng + * @version 20181012 + * + */ +public class CaseReport { + + /** + * 包含的测试步骤报告 + */ + private List stepReports = new ArrayList(); + + public List getStepReports() { + return stepReports; + } + + public void setStepReports(List stepReports) { + this.stepReports = stepReports; + } + + @Override + public String toString() { + return "CaseReport [stepReports=" + stepReports + "]"; + } +} diff --git a/hutool-json/src/test/java/cn/hutool/json/test/bean/report/EnvSettingInfo.java b/hutool-json/src/test/java/cn/hutool/json/test/bean/report/EnvSettingInfo.java new file mode 100644 index 000000000..5136a20b8 --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/test/bean/report/EnvSettingInfo.java @@ -0,0 +1,243 @@ +package cn.hutool.json.test.bean.report; + +import java.util.Collection; + +/** + * 测试环境信息 + * @author xuwangcheng + * @version 20181012 + * + */ +public class EnvSettingInfo { + + public static boolean DEV_MODE = true; + + private boolean remoteMode; + + private String hubRemoteUrl; + + private String reportFolder = "/report"; + private String screenshotFolder = "/screenshot";; + + private String elementFolder = "/config/element/"; + private String suiteFolder = "/config/suite/"; + + private String chromeDriverPath = "/src/main/resources/chromedriver.exe"; + private String ieDriverPath = "/src/main/resources/IEDriverServer.exe"; + private String operaDriverPath = "/src/main/resources/operadriver.exe"; + private String firefoxDriverPath = "/src/main/resources/geckodriver.exe"; + + private Double defaultSleepSeconds; + + private Integer elementLocationRetryCount; + private Double elementLocationTimeouts; + + /** + * 收件人列表 + */ + private Collection tos; + /** + * 抄送人列表 + */ + private Collection ccs; + /** + * 密送人列表 + */ + private Collection bccs; + + /** + * 是否可以开启定时任务 + */ + private boolean cronEnabled = false; + + /** + * 定时执行:suite文件 + */ + private String cronSuite; + + /** + * 定时执行:cron表达式,支持linux crontab格式(5位)和Quartz的cron格式(6位) + */ + private String cronExpression; + + /** + * 存储测试报告数据的轻量级数据库,路径 + */ + private String sqlitePath; + + public EnvSettingInfo() { + super(); + } + + public void setSqlitePath(String sqlitePath) { + this.sqlitePath = sqlitePath; + } + + public String getSqlitePath() { + return sqlitePath; + } + + public void setCronEnabled(boolean cronEnabled) { + this.cronEnabled = cronEnabled; + } + + public boolean isCronEnabled() { + return cronEnabled; + } + + public String getCronSuite() { + return cronSuite; + } + + public void setCronSuite(String cronSuite) { + this.cronSuite = cronSuite; + } + + public String getCronExpression() { + return cronExpression; + } + + public void setCronExpression(String cronExpression) { + this.cronExpression = cronExpression; + } + + public Integer getElementLocationRetryCount() { + return elementLocationRetryCount; + } + + public void setElementLocationRetryCount(Integer elementLocationRetryCount) { + this.elementLocationRetryCount = elementLocationRetryCount; + } + + public Double getElementLocationTimeouts() { + return elementLocationTimeouts; + } + + public void setElementLocationTimeouts(Double elementLocationTimeouts) { + this.elementLocationTimeouts = elementLocationTimeouts; + } + + public String getElementFolder() { + return elementFolder; + } + + public void setElementFolder(String elementFolder) { + this.elementFolder = elementFolder; + } + + public String getSuiteFolder() { + return suiteFolder; + } + + public void setSuiteFolder(String suiteFolder) { + this.suiteFolder = suiteFolder; + } + + public boolean isRemoteMode() { + return remoteMode; + } + + public void setRemoteMode(boolean remoteMode) { + this.remoteMode = remoteMode; + } + + public String getHubRemoteUrl() { + return hubRemoteUrl; + } + + public void setHubRemoteUrl(String hubRemoteUrl) { + this.hubRemoteUrl = hubRemoteUrl; + } + + public String getReportFolder() { + return reportFolder; + } + + public void setReportFolder(String reportFolder) { + this.reportFolder = reportFolder; + } + + public String getScreenshotFolder() { + return screenshotFolder; + } + + public void setScreenshotFolder(String screenshotFolder) { + this.screenshotFolder = screenshotFolder; + } + + public String getChromeDriverPath() { + return chromeDriverPath; + } + + public void setChromeDriverPath(String chromeDriverPath) { + this.chromeDriverPath = chromeDriverPath; + } + + public String getIeDriverPath() { + return ieDriverPath; + } + + public void setIeDriverPath(String ieDriverPath) { + this.ieDriverPath = ieDriverPath; + } + + public String getOperaDriverPath() { + return operaDriverPath; + } + + public void setOperaDriverPath(String operaDriverPath) { + this.operaDriverPath = operaDriverPath; + } + + public String getFirefoxDriverPath() { + return firefoxDriverPath; + } + + public void setFirefoxDriverPath(String firefoxDriverPath) { + this.firefoxDriverPath = firefoxDriverPath; + } + + public Double getDefaultSleepSeconds() { + return defaultSleepSeconds; + } + + public void setDefaultSleepSeconds(Double defaultSleepSeconds) { + this.defaultSleepSeconds = defaultSleepSeconds; + } + + public Collection getTos() { + return tos; + } + + public void setTos(Collection tos) { + this.tos = tos; + } + + public Collection getCcs() { + return ccs; + } + + public void setCcs(Collection ccs) { + this.ccs = ccs; + } + + public Collection getBccs() { + return bccs; + } + + public void setBccs(Collection bccs) { + this.bccs = bccs; + } + + @Override + public String toString() { + return "EnvSettingInfo [remoteMode=" + remoteMode + ", hubRemoteUrl=" + hubRemoteUrl + ", reportFolder=" + + reportFolder + ", screenshotFolder=" + screenshotFolder + ", elementFolder=" + elementFolder + + ", suiteFolder=" + suiteFolder + ", chromeDriverPath=" + chromeDriverPath + ", ieDriverPath=" + + ieDriverPath + ", operaDriverPath=" + operaDriverPath + ", firefoxDriverPath=" + firefoxDriverPath + + ", defaultSleepSeconds=" + defaultSleepSeconds + ", elementLocationRetryCount=" + + elementLocationRetryCount + ", elementLocationTimeouts=" + elementLocationTimeouts + ", mailAccount=" + + 1 + ", tos=" + tos + ", ccs=" + ccs + ", bccs=" + bccs + ", cronEnabled=" + cronEnabled + + ", cronSuite=" + cronSuite + ", cronExpression=" + cronExpression + "]"; + } +} diff --git a/hutool-json/src/test/java/cn/hutool/json/test/bean/report/StepReport.java b/hutool-json/src/test/java/cn/hutool/json/test/bean/report/StepReport.java new file mode 100644 index 000000000..ae0719170 --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/test/bean/report/StepReport.java @@ -0,0 +1,160 @@ +package cn.hutool.json.test.bean.report; + +import cn.hutool.core.util.StrUtil; + +/** + * 测试步骤报告 + * @author xuwangcheng + * @version 20181012 + * + */ +public class StepReport { + private static int step_id = 0; + + private int stepId = getId(); + /** + * 步骤名称 + */ + private String stepName; + /** + * 元素名称 + */ + private String elementName; + /** + * 元素定位器 + */ + private String location; + /** + * 参数 + */ + private String params; + /** + * 结果 + */ + private String result; + /** + * 操作名称,中文 + */ + private String actionName; + /** + * 测试时间 + */ + private String testTime; + + /** + * 测试状态:true-成功 false-失败 + */ + private boolean status = true; + /** + * 备注信息 + */ + private String mark; + /** + * 截图路径:相对路径 + */ + private String screenshot; + + private static synchronized int getId() { + return step_id++; + } + + public int getStepId() { + return stepId; + } + + public void setStepId(int stepId) { + this.stepId = stepId; + } + + public void setResult(String result) { + this.result = result; + } + + public String getResult() { + return result; + } + + public String getStepName() { + return stepName; + } + + public void setStepName(String stepName) { + this.stepName = stepName; + } + + public void setStepName() { + this.stepName = this.actionName + (StrUtil.isBlank(this.elementName) ? "" : " => " + this.elementName); + } + + public String getElementName() { + return elementName; + } + + public void setElementName(String elementName) { + this.elementName = elementName; + } + + public String getLocation() { + return location; + } + + public void setLocation(String location) { + this.location = location; + } + + public String getParams() { + return params; + } + + public void setParams(String params) { + this.params = params; + } + + public String getActionName() { + return actionName; + } + + public void setActionName(String actionName) { + this.actionName = actionName; + } + + public String getTestTime() { + return testTime; + } + + public void setTestTime(String testTime) { + this.testTime = testTime; + } + + public boolean isStatus() { + return status; + } + + public void setStatus(boolean status) { + this.status = status; + } + + public String getMark() { + return mark; + } + + public void setMark(String mark) { + this.mark = mark; + } + + public String getScreenshot() { + return screenshot; + } + + public void setScreenshot(String screenshot) { + this.screenshot = screenshot; + } + + @Override + public String toString() { + return "StepReport [stepId=" + stepId + ", stepName=" + stepName + ", elementName=" + elementName + + ", location=" + location + ", params=" + params + ", result=" + result + ", actionName=" + actionName + + ", testTime=" + testTime + ", status=" + status + ", mark=" + mark + ", screenshot=" + screenshot + + "]"; + } +} diff --git a/hutool-json/src/test/java/cn/hutool/json/test/bean/report/SuiteReport.java b/hutool-json/src/test/java/cn/hutool/json/test/bean/report/SuiteReport.java new file mode 100644 index 000000000..df9589710 --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/test/bean/report/SuiteReport.java @@ -0,0 +1,31 @@ +package cn.hutool.json.test.bean.report; + +import java.util.ArrayList; +import java.util.List; + +/** + * 测试套件报告 + * @author xuwangcheng + * @version 20181012 + * + */ +public class SuiteReport { + + /** + * 包含的用例测试报告 + */ + private List caseReports = new ArrayList(); + + public List getCaseReports() { + return caseReports; + } + + public void setCaseReports(List caseReports) { + this.caseReports = caseReports; + } + + @Override + public String toString() { + return "SuiteReport [caseReports=" + caseReports + "]"; + } +} diff --git a/hutool-json/src/test/resources/evaluation.json b/hutool-json/src/test/resources/evaluation.json new file mode 100644 index 000000000..4bd325f6b --- /dev/null +++ b/hutool-json/src/test/resources/evaluation.json @@ -0,0 +1,126 @@ +{ + "HF003": "武汉市", + "HF002": "585.37", + "HF005": "", + "HF004": "154", + "costTotal": 12, + "HF001": "北京市", + "HF007": "北京市", + "HF006": "0", + "HF009": "", + "HF008": "武汉市", + "HH010": "0", + "resResult": 0, + "HH011": "394.3", + "HH012": "7", + "HH013": "0", + "HH014": "0", + "XT_NO": "2017101714250003", + "resMsg": "success", + "HH008": "0", + "HH009": "0", + "HH006": "0", + "HH007": "0", + "HH004": "0", + "HH005": "0", + "HH002": "1", + "HH003": "0", + "HH001": "38.93", + "resCode": "0000", + "HD002": "77", + "HD001": "1", + "HG008": "0", + "HE004": "0", + "HG007": "0", + "HE003": "0", + "HE002": "2", + "HG009": "0", + "HE001": "1", + "HG004": "4", + "HE008": "139.55", + "HG003": "164.18", + "HE007": "50", + "HG006": "1", + "HD008": "0", + "HE006": "0", + "HG005": "77", + "HE005": "0", + "HD007": "0", + "HD004": "0.0", + "HG002": "7", + "HE009": "0", + "HD005": "0.0", + "HG001": "259.4", + "HE011": "0", + "HE010": "0", + "HE013": "1", + "HE012": "0", + "HE015": "0", + "HE014": "0", + "HE017": "3", + "HG016": "商户", + "HE016": "0", + "HG015": "商户", + "HE019": "20", + "HG014": "餐馆", + "HE018": "2", + "HG013": "计算机商店", + "HG012": "仓储大超市", + "HG011": "3", + "HG010": "0", + "HB001": "1", + "HB003": "631", + "HB002": "492", + "HE026": "1", + "HE025": "1", + "HE024": "59.76", + "HE023": "93.83", + "HE022": "0", + "HE021": "0", + "XT_MONTH02": "2017/04", + "HE020": "0", + "XT_MONTH01": "2017/03", + "HE029": "0", + "CARD_HOLDER": "王雁", + "HE028": "0", + "HE027": "0", + "CARD_NO": "6225768752646560", + "HC016": "103.02", + "HC015": "93.83", + "HC014": "0", + "HC013": "0", + "HC012": "213.73", + "HC011": "90", + "HC010": "3", + "HE035": "0", + "HE034": "0", + "HE036": "43.26", + "HD010": "0", + "HE031": "20", + "HD011": "0", + "HE030": "1", + "HE033": "0", + "HD013": "11.00", + "HE032": "74.18", + "HD014": "423.58", + "HA002": "金卡", + "HA001": "招商银行", + "HD016": "8", + "HA004": "4", + "HD017": "7", + "HF010": "8", + "HA003": "招商银行信用卡", + "HC008": "0", + "HC007": "0", + "HC009": "3", + "HC004": "83.43", + "HC003": "2.0", + "HC006": "3", + "HC005": "3", + "XT_MONTH05": "2017/07", + "XT_MONTH06": "2017/08", + "HC002": "500.58", + "XT_MONTH03": "2017/05", + "HC001": "12", + "XT_MONTH04": "2017/06" +} \ No newline at end of file diff --git a/hutool-json/src/test/resources/exam_test.json b/hutool-json/src/test/resources/exam_test.json new file mode 100644 index 000000000..e61a705b3 --- /dev/null +++ b/hutool-json/src/test/resources/exam_test.json @@ -0,0 +1,281 @@ +[ + { + "id": 1753, + "examNumber": 1, + "isAnswer": true, + "answerArray": [ + { + "seq": 0 + } + ], + "isRight": 1, + "isSubject": 0 + }, + { + "id": 1754, + "examNumber": 2, + "isAnswer": true, + "answerArray": [ + { + "seq": 1 + } + ], + "isRight": 0, + "isSubject": 0 + }, + { + "id": 1756, + "examNumber": 3, + "isAnswer": true, + "answerArray": [ + { + "seq": 1 + }, + { + "seq": 2 + } + ], + "isRight": 0, + "isSubject": 0 + }, + { + "id": 1757, + "examNumber": 4, + "isAnswer": true, + "answerArray": [ + { + "seq": 1 + }, + { + "seq": 3 + } + ], + "isRight": 0, + "isSubject": 0 + }, + { + "id": 1759, + "examNumber": 5, + "isAnswer": true, + "answerArray": [ + { + "seq": 0 + } + ], + "isRight": 1, + "isSubject": 0 + }, + { + "id": 1760, + "examNumber": 6, + "isAnswer": true, + "answerArray": [ + { + "seq": 1 + } + ], + "isRight": 0, + "isSubject": 0 + }, + { + "id": 1762, + "examNumber": 7, + "isAnswer": true, + "answerArray": [ + { + "detailContent": "公司的发生地方" + }, + { + "detailContent": "告诉对方" + }, + { + "detailContent": "二位水电费" + }, + { + "detailContent": "公司东方闪电" + } + ], + "isRight": 0, + "isSubject": 0 + }, + { + "id": 1763, + "examNumber": 8, + "isAnswer": true, + "answerArray": [ + { + "detailContent": "告诉对方s" + }, + { + "detailContent": "公司垫付多少" + } + ], + "isRight": 0, + "isSubject": 0 + }, + { + "id": 1765, + "examNumber": 9, + "isAnswer": true, + "answerArray": [ + { + "detailContent": "http://pic6.huitu.com/res/20130116/84481_20130116142820494200_1.jpg,http://pic6.huitu.com/res/20130116/84481_20130116142820494200_1.jpg" + } + ], + "isSubject": 1 + }, + { + "id": 1766, + "examNumber": 10, + "isAnswer": true, + "answerArray": [ + { + "detailContent": "http://pic6.huitu.com/res/20130116/84481_20130116142820494200_1.jpg,http://…_1.jpg,http://pic6.huitu.com/res/20130116/84481_20130116142820494200_1.jpg" + } + ], + "isSubject": 1 + }, + { + "id": 0, + "examNumber": 11, + "isAnswer": true, + "answerArray": [ + { + "seq": 0 + } + ], + "isRight": 1, + "isSubject": 0 + }, + { + "id": 1, + "examNumber": 12, + "isAnswer": true, + "answerArray": [ + { + "seq": 1 + } + ], + "isRight": 0, + "isSubject": 0 + }, + { + "id": 2, + "examNumber": 13, + "isAnswer": true, + "answerArray": [ + { + "seq": 0 + } + ], + "isRight": 1, + "isSubject": 0 + }, + { + "id": 3, + "examNumber": 14, + "isAnswer": true, + "answerArray": [ + { + "seq": 1 + } + ], + "isRight": 0, + "isSubject": 0 + }, + { + "id": 4, + "examNumber": 15, + "isAnswer": true, + "answerArray": [ + { + "detailContent": "公司东方闪电" + }, + { + "detailContent": "国顺副食店" + }, + { + "detailContent": "无二水电费是的" + }, + { + "detailContent": "公司东方闪电" + } + ], + "isRight": 0, + "isSubject": 0 + }, + { + "id": 5, + "examNumber": 16, + "isAnswer": true, + "answerArray": [ + { + "detailContent": "公司东方闪电" + }, + { + "detailContent": "格式的维尔士大夫" + }, + { + "detailContent": "公司东风" + } + ], + "isRight": 0, + "isSubject": 0 + }, + { + "id": 6, + "examNumber": 17, + "isAnswer": true, + "answerArray": [ + { + "detailContent": "http://pic6.huitu.com/res/20130116/84481_20130116142820494200_1.jpg" + } + ], + "isSubject": 1 + }, + { + "id": 0, + "examNumber": 18, + "isAnswer": true, + "answerArray": [ + { + "detailContent": "告诉对方" + }, + { + "detailContent": "个涉外收电费" + } + ], + "isRight": 0, + "isSubject": 0 + }, + { + "id": 0, + "examNumber": 19, + "isAnswer": true, + "answerArray": [ + { + "detailContent": "公司发的" + }, + { + "detailContent": "公司东方闪电" + } + ], + "isRight": 0, + "isSubject": 0 + }, + { + "id": 1774, + "examNumber": 20, + "isAnswer": true, + "answerArray": [ + { + "detailContent": "好改送的非官方的" + }, + { + "detailContent": "和是发送到发送到" + } + ], + "isRight": 0, + "isSubject": 0 + } +] \ No newline at end of file diff --git a/hutool-json/src/test/resources/issueIVMD5.json b/hutool-json/src/test/resources/issueIVMD5.json new file mode 100644 index 000000000..f174df90d --- /dev/null +++ b/hutool-json/src/test/resources/issueIVMD5.json @@ -0,0 +1,81 @@ +{ + "result": 1, + "pagination": { + "maxPage": "2057", + "totalDataCount": "4114" + }, + "data": [ + { + "birthday": "", + "linkPhone": "", + "nation": "01", + "studentCode": "20170001", + "unitiveCode": "20170001", + "sex": "2", + "linkAddress": "", + "identityCard": "", + "accountId": "B4DDF491FDF34074AE7A819E1341CB6C", + "classId": "8c38f49c4362479cad59fdd5966fed04", + "password": "25d55ad283aa400af464c76d713c07ad", + "modifyTime": "20190311152351126", + "isDeleted": "0", + "postalcode": "", + "background": "", + "schoolId": "D23E00EBA6364220A17BBDB92A361ACA", + "studentName": "蓝有城", + "sequenceIntId": "1", + "nativePlace": "", + "id": "2C90488969524E9901696BA2E3D60005", + "username": "s0000000002894659" + }, + { + "birthday": "20010421000000", + "linkPhone": "", + "nation": "01", + "studentCode": "16000001310001", + "unitiveCode": "G452226200104219223", + "sex": "2", + "linkAddress": "", + "identityCard": "", + "accountId": "", + "classId": "8dbcc4906abc484a83cc0e1e1f0d91a3", + "password": "", + "modifyTime": "20190327201832000", + "isDeleted": "0", + "postalcode": "546100", + "background": "", + "schoolId": "D23E00EBA6364220A17BBDB92A361ACA", + "studentName": "韦奇秀", + "sequenceIntId": "21", + "nativePlace": "451302", + "id": "2C90488969BD091C0169BEF12988002A", + "username": "" + } + ], + "data2": { + "birthday": "", + "linkPhone": "", + "nation": "01", + "studentCode": "20170001", + "unitiveCode": "20170001", + "sex": "2", + "linkAddress": "", + "identityCard": "", + "accountId": "B4DDF491FDF34074AE7A819E1341CB6C", + "classId": "8c38f49c4362479cad59fdd5966fed04", + "password": "25d55ad283aa400af464c76d713c07ad", + "modifyTime": "20190311152351126", + "isDeleted": "0", + "postalcode": "", + "background": "", + "schoolId": "D23E00EBA6364220A17BBDB92A361ACA", + "studentName": "蓝有城", + "sequenceIntId": "1", + "nativePlace": "", + "id": "2C90488969524E9901696BA2E3D60005", + "username": "s0000000002894659" + }, + "nextDataUri": "", + "message": "ok", + "dataCount": 2 +} diff --git a/hutool-json/src/test/resources/suiteReport.json b/hutool-json/src/test/resources/suiteReport.json new file mode 100644 index 000000000..06af68185 --- /dev/null +++ b/hutool-json/src/test/resources/suiteReport.json @@ -0,0 +1,115 @@ +{ + "reportName": "Web自动化_20181104194718", + "failCount": 0, + "finished": true, + "title": "Web自动化", + "totalCount": 1, + "env": { + "cronSuite": "testsuite", + "firefoxDriverPath": "F:\\Eclipse2017Workplace\\MasterYIUITest/src/main/resources/geckodriver.exe", + "mailAccount": { + "charset": "UTF-8", + "debug": false, + "auth": true, + "pass": "q1w2e3", + "socketFactoryFallback": false, + "socketFactoryPort": 465, + "startttlsEnable": false, + "port": 25, + "socketFactoryClass": "javax.net.ssl.SSLSocketFactory", + "host": "smtp.yeah.net", + "splitlongparameters": false, + "from": "hutool@yeah.net", + "user": "hutool" + }, + "remoteMode": false, + "hubRemoteUrl": "http://192.168.2.40:4444/wd/hub", + "screenshotFolder": "/screenshot", + "cronEnabled": true, + "cronExpression": "*/2 * * * *", + "elementFolder": "F:\\Eclipse2017Workplace\\MasterYIUITest/config/element/", + "ccs": [""], + "elementLocationRetryCount": 3, + "sqlitePath": "F:\\Eclipse2017Workplace\\MasterYIUITest\\report.db", + "defaultSleepSeconds": 0.5, + "suiteFolder": "F:\\Eclipse2017Workplace\\MasterYIUITest/config/suite/", + "tos": ["309873223@qq.com", "610421185@qq.com", "xuwangcheng14@163.com"], + "ieDriverPath": "F:\\Eclipse2017Workplace\\MasterYIUITest/src/main/resources/IEDriverServer.exe", + "elementLocationTimeouts": 9, + "reportFolder": "F:\\Eclipse2017Workplace\\MasterYIUITest/report", + "operaDriverPath": "F:\\Eclipse2017Workplace\\MasterYIUITest/src/main/resources/operadriver.exe", + "chromeDriverPath": "F:\\Eclipse2017Workplace\\MasterYIUITest/src/main/resources/chromedriver.exe", + "bccs": [""] + }, + "browserName": ["chrome"], + "successCount": 1, + "useTime": 11069, + "testTime": "2018-11-04 19:47:07", + "endTime": "2018-11-04 19:47:18", + "skipCount": 0, + "caseReports": [{ + "finishTime": "2018-11-04 19:47:18", + "caseName": "百度搜索", + "useTime": "6320", + "runCount": 0, + "caseMethodPath": "com.dcits.test.baidu.usecase.Baidu.search", + "stepReports": [{ + "stepName": "打开Url地址", + "stepId": 0, + "testTime": "2018-11-04 19:47:11", + "params": "https://www.baidu.com/", + "actionName": "打开Url地址", + "status": true + }, { + "stepName": "输入 => 搜索框", + "stepId": 1, + "location": "id => kw", + "testTime": "2018-11-04 19:47:12", + "params": "xuwangcheng.com", + "elementName": "搜索框", + "actionName": "输入", + "status": true + }, { + "stepName": "点击 => 搜索按钮", + "stepId": 2, + "location": "id => su", + "testTime": "2018-11-04 19:47:14", + "params": "", + "elementName": "搜索按钮", + "actionName": "点击", + "status": true + }, { + "stepName": "点击 => 搜索结果", + "stepId": 3, + "location": "xpath => //*[@id=\"1\"]/h3/a", + "testTime": "2018-11-04 19:47:15", + "params": "", + "elementName": "搜索结果", + "actionName": "点击", + "status": true + }, { + "stepName": "切换到指定窗口", + "stepId": 4, + "testTime": "2018-11-04 19:47:17", + "params": "1", + "actionName": "切换到指定窗口", + "status": true + }, { + "stepName": "切换到指定窗口", + "stepId": 5, + "testTime": "2018-11-04 19:47:17", + "params": "0", + "actionName": "切换到指定窗口", + "status": true + }, { + "stepName": "刷新页面", + "stepId": 6, + "testTime": "2018-11-04 19:47:17", + "params": "", + "actionName": "刷新页面", + "status": true + }], + "browserType": "chrome", + "status": "success" + }] +} \ No newline at end of file diff --git a/hutool-log/pom.xml b/hutool-log/pom.xml new file mode 100644 index 000000000..f9c6f86a1 --- /dev/null +++ b/hutool-log/pom.xml @@ -0,0 +1,81 @@ + + + 4.0.0 + + jar + + + cn.hutool + hutool-parent + 4.6.2-SNAPSHOT + + + hutool-log + ${project.artifactId} + Hutool 日志封装 + + + + 1.7.26 + 1.2.3 + 1.2.17 + 2.11.2 + 1.2 + 1.3.5 + 3.3.2.Final + + + + + cn.hutool + hutool-core + ${project.parent.version} + + + + + org.slf4j + slf4j-api + ${slf4j.version} + true + + + ch.qos.logback + logback-classic + ${logback.version} + true + + + log4j + log4j + ${log4j.version} + true + + + org.apache.logging.log4j + log4j-core + ${log4j2.version} + true + + + commons-logging + commons-logging + ${commons-logging.version} + true + + + org.tinylog + tinylog + ${tinylog.version} + true + + + org.jboss.logging + jboss-logging + ${jboss-logging.version} + true + + + diff --git a/hutool-log/src/main/java/cn/hutool/log/AbstractLog.java b/hutool-log/src/main/java/cn/hutool/log/AbstractLog.java new file mode 100644 index 000000000..b3b8d8ffb --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/AbstractLog.java @@ -0,0 +1,148 @@ +package cn.hutool.log; + +import java.io.Serializable; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.log.level.Level; + +/** + * 抽象日志类
+ * 实现了一些通用的接口 + * + * @author Looly + * + */ +public abstract class AbstractLog implements Log, Serializable{ + + private static final long serialVersionUID = -3211115409504005616L; + private static final String FQCN = AbstractLog.class.getName(); + + @Override + public boolean isEnabled(Level level) { + switch (level) { + case TRACE: + return isTraceEnabled(); + case DEBUG: + return isDebugEnabled(); + case INFO: + return isInfoEnabled(); + case WARN: + return isWarnEnabled(); + case ERROR: + return isErrorEnabled(); + default: + throw new Error(StrUtil.format("Can not identify level: {}", level)); + } + } + + @Override + public void trace(Throwable t) { + trace(t, ExceptionUtil.getSimpleMessage(t)); + } + + @Override + public void trace(String format, Object... arguments) { + trace(null, format, arguments); + } + + @Override + public void trace(Throwable t, String format, Object... arguments) { + trace(FQCN, t, format, arguments); + } + + @Override + public void debug(Throwable t) { + debug(t, ExceptionUtil.getSimpleMessage(t)); + } + + @Override + public void debug(String format, Object... arguments) { + if(null != arguments && 1 == arguments.length && arguments[0] instanceof Throwable) { + // 兼容Slf4j中的xxx(String message, Throwable e) + debug((Throwable)arguments[0], format); + } else { + debug(null, format, arguments); + } + } + + @Override + public void debug(Throwable t, String format, Object... arguments) { + debug(FQCN, t, format, arguments); + } + + @Override + public void info(Throwable t) { + info(t, ExceptionUtil.getSimpleMessage(t)); + } + + @Override + public void info(String format, Object... arguments) { + if(null != arguments && 1 == arguments.length && arguments[0] instanceof Throwable) { + // 兼容Slf4j中的xxx(String message, Throwable e) + info((Throwable)arguments[0], format); + } else { + info(null, format, arguments); + } + } + + @Override + public void info(Throwable t, String format, Object... arguments) { + info(FQCN, t, format, arguments); + } + + @Override + public void warn(Throwable t) { + warn(t, ExceptionUtil.getSimpleMessage(t)); + } + + @Override + public void warn(String format, Object... arguments) { + if(null != arguments && 1 == arguments.length && arguments[0] instanceof Throwable) { + // 兼容Slf4j中的xxx(String message, Throwable e) + warn((Throwable)arguments[0], format); + } else { + warn(null, format, arguments); + } + } + + @Override + public void warn(Throwable t, String format, Object... arguments) { + warn(FQCN, t, format, arguments); + } + + @Override + public void error(Throwable t) { + this.error(t, ExceptionUtil.getSimpleMessage(t)); + } + + @Override + public void error(String format, Object... arguments) { + if(null != arguments && 1 == arguments.length && arguments[0] instanceof Throwable) { + // 兼容Slf4j中的xxx(String message, Throwable e) + error((Throwable)arguments[0], format); + } else { + error(null, format, arguments); + } + } + + @Override + public void error(Throwable t, String format, Object... arguments) { + error(FQCN, t, format, arguments); + } + + @Override + public void log(Level level, String format, Object... arguments) { + if(null != arguments && 1 == arguments.length && arguments[0] instanceof Throwable) { + // 兼容Slf4j中的xxx(String message, Throwable e) + log(level, (Throwable)arguments[0], format); + } else { + log(level, null, format, arguments); + } + } + + @Override + public void log(Level level, Throwable t, String format, Object... arguments) { + this.log(FQCN, level, t, format, arguments); + } +} diff --git a/hutool-log/src/main/java/cn/hutool/log/GlobalLogFactory.java b/hutool-log/src/main/java/cn/hutool/log/GlobalLogFactory.java new file mode 100644 index 000000000..30712abb7 --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/GlobalLogFactory.java @@ -0,0 +1,76 @@ +package cn.hutool.log; + +import cn.hutool.log.dialect.commons.ApacheCommonsLogFactory; +import cn.hutool.log.dialect.console.ConsoleLogFactory; +import cn.hutool.log.dialect.jdk.JdkLogFactory; +import cn.hutool.log.dialect.log4j.Log4jLogFactory; +import cn.hutool.log.dialect.log4j2.Log4j2LogFactory; +import cn.hutool.log.dialect.slf4j.Slf4jLogFactory; + +/** + * 全局日志工厂类
+ * 用于减少日志工厂创建,减少日志库探测 + * + * @author looly + * @since 4.0.3 + */ +public class GlobalLogFactory { + private static volatile LogFactory currentLogFactory; + private static final Object lock = new Object(); + + /** + * 获取单例日志工厂类,如果不存在创建之 + * + * @return 当前使用的日志工厂 + */ + public static LogFactory get() { + if (null == currentLogFactory) { + synchronized (lock) { + if (null == currentLogFactory) { + currentLogFactory = LogFactory.create(); + } + } + } + return currentLogFactory; + } + + /** + * 自定义日志实现 + * + * @see Slf4jLogFactory + * @see Log4jLogFactory + * @see Log4j2LogFactory + * @see ApacheCommonsLogFactory + * @see JdkLogFactory + * @see ConsoleLogFactory + * + * @param logFactoryClass 日志工厂类 + * @return 自定义的日志工厂类 + */ + public static LogFactory set(Class logFactoryClass) { + try { + return set(logFactoryClass.newInstance()); + } catch (Exception e) { + throw new IllegalArgumentException("Can not instance LogFactory class!", e); + } + } + + /** + * 自定义日志实现 + * + * @see Slf4jLogFactory + * @see Log4jLogFactory + * @see Log4j2LogFactory + * @see ApacheCommonsLogFactory + * @see JdkLogFactory + * @see ConsoleLogFactory + * + * @param logFactory 日志工厂类对象 + * @return 自定义的日志工厂类 + */ + public static LogFactory set(LogFactory logFactory) { + logFactory.getLog(GlobalLogFactory.class).debug("Custom Use [{}] Logger.", logFactory.name); + currentLogFactory = logFactory; + return currentLogFactory; + } +} diff --git a/hutool-log/src/main/java/cn/hutool/log/Log.java b/hutool-log/src/main/java/cn/hutool/log/Log.java new file mode 100644 index 000000000..54664cdac --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/Log.java @@ -0,0 +1,58 @@ +package cn.hutool.log; + +import cn.hutool.log.level.DebugLog; +import cn.hutool.log.level.ErrorLog; +import cn.hutool.log.level.InfoLog; +import cn.hutool.log.level.Level; +import cn.hutool.log.level.TraceLog; +import cn.hutool.log.level.WarnLog; + +/** + * 日志统一接口 + * + * @author Looly + * + */ +public interface Log extends TraceLog, DebugLog, InfoLog, WarnLog, ErrorLog { + + /** + * @return 日志对象的Name + */ + public String getName(); + + /** + * 是否开启指定日志 + * @param level 日志级别 + * @return 是否开启指定级别 + */ + boolean isEnabled(Level level); + + /** + * 打印指定级别的日志 + * @param level 级别 + * @param format 消息模板 + * @param arguments 参数 + */ + void log(Level level, String format, Object... arguments); + + /** + * 打印 指定级别的日志 + * + * @param level 级别 + * @param t 错误对象 + * @param format 消息模板 + * @param arguments 参数 + */ + void log(Level level, Throwable t, String format, Object... arguments); + + /** + * 打印 ERROR 等级的日志 + * + * @param fqcn 完全限定类名(Fully Qualified Class Name),用于定位日志位置 + * @param level 级别 + * @param t 错误对象 + * @param format 消息模板 + * @param arguments 参数 + */ + void log(String fqcn, Level level, Throwable t, String format, Object... arguments); +} diff --git a/hutool-log/src/main/java/cn/hutool/log/LogFactory.java b/hutool-log/src/main/java/cn/hutool/log/LogFactory.java new file mode 100644 index 000000000..fa00087a4 --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/LogFactory.java @@ -0,0 +1,263 @@ +package cn.hutool.log; + +import java.net.URL; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.lang.caller.CallerUtil; +import cn.hutool.log.dialect.commons.ApacheCommonsLogFactory; +import cn.hutool.log.dialect.console.ConsoleLogFactory; +import cn.hutool.log.dialect.jboss.JbossLogFactory; +import cn.hutool.log.dialect.jdk.JdkLogFactory; +import cn.hutool.log.dialect.log4j.Log4jLogFactory; +import cn.hutool.log.dialect.log4j2.Log4j2LogFactory; +import cn.hutool.log.dialect.slf4j.Slf4jLogFactory; +import cn.hutool.log.dialect.tinylog.TinyLogFactory; + +/** + * 日志工厂类 + * + * @see Slf4jLogFactory + * @see Log4j2LogFactory + * @see Log4jLogFactory + * @see ApacheCommonsLogFactory + * @see TinyLogFactory + * @see JbossLogFactory + * @see ConsoleLogFactory + * @see JdkLogFactory + * + * @author Looly + * + */ +public abstract class LogFactory { + + /** 日志框架名,用于打印当前所用日志框架 */ + protected String name; + /** 日志对象缓存 */ + private Map logCache; + + /** + * 构造 + * + * @param name 日志框架名 + */ + public LogFactory(String name) { + this.name = name; + logCache = new ConcurrentHashMap<>(); + } + + /** + * 获取日志框架名,用于打印当前所用日志框架 + * + * @return 日志框架名 + * @since 4.1.21 + */ + public String getName() { + return this.name; + } + + /** + * 获得日志对象 + * + * @param name 日志对象名 + * @return 日志对象 + */ + public Log getLog(String name) { + Log log = logCache.get(name); + if (null == log) { + log = createLog(name); + logCache.put(name, log); + } + return log; + } + + /** + * 获得日志对象 + * + * @param clazz 日志对应类 + * @return 日志对象 + */ + public Log getLog(Class clazz) { + Log log = logCache.get(clazz); + if (null == log) { + log = createLog(clazz); + logCache.put(clazz, log); + } + return log; + } + + /** + * 创建日志对象 + * + * @param name 日志对象名 + * @return 日志对象 + */ + public abstract Log createLog(String name); + + /** + * 创建日志对象 + * + * @param clazz 日志对应类 + * @return 日志对象 + */ + public abstract Log createLog(Class clazz); + + /** + * 检查日志实现是否存在
+ * 此方法仅用于检查所提供的日志相关类是否存在,当传入的日志类类不存在时抛出ClassNotFoundException
+ * 此方法的作用是在detectLogFactory方法自动检测所用日志时,如果实现类不存在,调用此方法会自动抛出异常,从而切换到下一种日志的检测。 + * + * @param logClassName 日志实现相关类 + */ + protected void checkLogExist(Class logClassName) { + // 不做任何操作 + } + + // ------------------------------------------------------------------------- Static start + /** + * @return 当前使用的日志工厂 + */ + public static LogFactory getCurrentLogFactory() { + return GlobalLogFactory.get(); + } + + /** + * 自定义日志实现 + * + * @see Slf4jLogFactory + * @see Log4j2LogFactory + * @see Log4jLogFactory + * @see ApacheCommonsLogFactory + * @see TinyLogFactory + * @see JbossLogFactory + * @see ConsoleLogFactory + * @see JdkLogFactory + * + * @param logFactoryClass 日志工厂类 + * @return 自定义的日志工厂类 + */ + public static LogFactory setCurrentLogFactory(Class logFactoryClass) { + return GlobalLogFactory.set(logFactoryClass); + } + + /** + * 自定义日志实现 + * + * @see Slf4jLogFactory + * @see Log4j2LogFactory + * @see Log4jLogFactory + * @see ApacheCommonsLogFactory + * @see TinyLogFactory + * @see JbossLogFactory + * @see ConsoleLogFactory + * @see JdkLogFactory + * + * @param logFactory 日志工厂类对象 + * @return 自定义的日志工厂类 + */ + public static LogFactory setCurrentLogFactory(LogFactory logFactory) { + return GlobalLogFactory.set(logFactory); + } + + /** + * 获得日志对象 + * + * @param name 日志对象名 + * @return 日志对象 + */ + public static Log get(String name) { + return getCurrentLogFactory().getLog(name); + } + + /** + * 获得日志对象 + * + * @param clazz 日志对应类 + * @return 日志对象 + */ + public static Log get(Class clazz) { + return getCurrentLogFactory().getLog(clazz); + } + + /** + * @return 获得调用者的日志 + */ + public static Log get() { + return get(CallerUtil.getCallerCaller()); + } + + /** + * 决定日志实现 + *

+ * 依次按照顺序检查日志库的jar是否被引入,如果未引入任何日志库,则检查ClassPath下的logging.properties,存在则使用JdkLogFactory,否则使用ConsoleLogFactory + * + * @see Slf4jLogFactory + * @see Log4j2LogFactory + * @see Log4jLogFactory + * @see ApacheCommonsLogFactory + * @see TinyLogFactory + * @see JbossLogFactory + * @see ConsoleLogFactory + * @see JdkLogFactory + * @return 日志实现类 + */ + public static LogFactory create() { + final LogFactory factory = doCreate(); + factory.getLog(LogFactory.class).debug("Use [{}] Logger As Default.", factory.name); + return factory; + } + + /** + * 决定日志实现 + *

+ * 依次按照顺序检查日志库的jar是否被引入,如果未引入任何日志库,则检查ClassPath下的logging.properties,存在则使用JdkLogFactory,否则使用ConsoleLogFactory + * + * @see Slf4jLogFactory + * @see Log4j2LogFactory + * @see Log4jLogFactory + * @see ApacheCommonsLogFactory + * @see TinyLogFactory + * @see JbossLogFactory + * @see ConsoleLogFactory + * @see JdkLogFactory + * @return 日志实现类 + */ + private static LogFactory doCreate() { + try { + return new Slf4jLogFactory(true); + } catch (NoClassDefFoundError e) { + // ignore + } + try { + return new Log4j2LogFactory(); + } catch (NoClassDefFoundError e) { + // ignore + } + try { + return new Log4jLogFactory(); + } catch (NoClassDefFoundError e) { + // ignore + } + try { + return new ApacheCommonsLogFactory(); + } catch (NoClassDefFoundError e) { + // ignore + } + try { + return new TinyLogFactory(); + } catch (NoClassDefFoundError e) { + // ignore + } + try { + return new JbossLogFactory(); + } catch (NoClassDefFoundError e) { + // ignore + } + + // 未找到任何可支持的日志库时判断依据:当JDK Logging的配置文件位于classpath中,使用JDK Logging,否则使用Console + final URL url = ResourceUtil.getResource("logging.properties"); + return (null != url) ? new JdkLogFactory() : new ConsoleLogFactory(); + } + // ------------------------------------------------------------------------- Static end +} diff --git a/hutool-log/src/main/java/cn/hutool/log/StaticLog.java b/hutool-log/src/main/java/cn/hutool/log/StaticLog.java new file mode 100644 index 000000000..49f7d07d3 --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/StaticLog.java @@ -0,0 +1,244 @@ +package cn.hutool.log; + +import cn.hutool.core.lang.caller.CallerUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.log.level.Level; + +/** + * 静态日志类,用于在不引入日志对象的情况下打印日志 + * + * @author Looly + * + */ +public final class StaticLog { + private static final String FQCN = StaticLog.class.getName(); + + private StaticLog() { + } + + // ----------------------------------------------------------- Log method start + // ------------------------ Trace + /** + * Trace等级日志,小于debug
+ * 由于动态获取Log,效率较低,建议在非频繁调用的情况下使用!! + * + * @param format 格式文本,{} 代表变量 + * @param arguments 变量对应的参数 + */ + public static void trace(String format, Object... arguments) { + trace(LogFactory.get(CallerUtil.getCallerCaller()), format, arguments); + } + + /** + * Trace等级日志,小于Debug + * + * @param log 日志对象 + * @param format 格式文本,{} 代表变量 + * @param arguments 变量对应的参数 + */ + public static void trace(Log log, String format, Object... arguments) { + log.trace(FQCN, null, format, arguments); + } + + // ------------------------ debug + /** + * Debug等级日志,小于Info
+ * 由于动态获取Log,效率较低,建议在非频繁调用的情况下使用!! + * + * @param format 格式文本,{} 代表变量 + * @param arguments 变量对应的参数 + */ + public static void debug(String format, Object... arguments) { + debug(LogFactory.get(CallerUtil.getCallerCaller()), format, arguments); + } + + /** + * Debug等级日志,小于Info + * + * @param log 日志对象 + * @param format 格式文本,{} 代表变量 + * @param arguments 变量对应的参数 + */ + public static void debug(Log log, String format, Object... arguments) { + log.debug(FQCN, null, format, arguments); + } + + // ------------------------ info + /** + * Info等级日志,小于Warn
+ * 由于动态获取Log,效率较低,建议在非频繁调用的情况下使用!! + * + * @param format 格式文本,{} 代表变量 + * @param arguments 变量对应的参数 + */ + public static void info(String format, Object... arguments) { + info(LogFactory.get(CallerUtil.getCallerCaller()), format, arguments); + } + + /** + * Info等级日志,小于Warn + * + * @param log 日志对象 + * @param format 格式文本,{} 代表变量 + * @param arguments 变量对应的参数 + */ + public static void info(Log log, String format, Object... arguments) { + log.info(FQCN, null, format, arguments); + } + + // ------------------------ warn + /** + * Warn等级日志,小于Error
+ * 由于动态获取Log,效率较低,建议在非频繁调用的情况下使用!! + * + * @param format 格式文本,{} 代表变量 + * @param arguments 变量对应的参数 + */ + public static void warn(String format, Object... arguments) { + warn(LogFactory.get(CallerUtil.getCallerCaller()), format, arguments); + } + + /** + * Warn等级日志,小于Error
+ * 由于动态获取Log,效率较低,建议在非频繁调用的情况下使用!! + * + * @param e 需在日志中堆栈打印的异常 + * @param format 格式文本,{} 代表变量 + * @param arguments 变量对应的参数 + */ + public static void warn(Throwable e, String format, Object... arguments) { + warn(LogFactory.get(CallerUtil.getCallerCaller()), e, StrUtil.format(format, arguments)); + } + + /** + * Warn等级日志,小于Error + * + * @param log 日志对象 + * @param format 格式文本,{} 代表变量 + * @param arguments 变量对应的参数 + */ + public static void warn(Log log, String format, Object... arguments) { + warn(log, null, format, arguments); + } + + /** + * Warn等级日志,小于Error + * + * @param log 日志对象 + * @param e 需在日志中堆栈打印的异常 + * @param format 格式文本,{} 代表变量 + * @param arguments 变量对应的参数 + */ + public static void warn(Log log, Throwable e, String format, Object... arguments) { + log.warn(FQCN, e, format, arguments); + } + + // ------------------------ error + /** + * Error等级日志
+ * 由于动态获取Log,效率较低,建议在非频繁调用的情况下使用!! + * + * @param e 需在日志中堆栈打印的异常 + */ + public static void error(Throwable e) { + error(LogFactory.get(CallerUtil.getCallerCaller()), e); + } + + /** + * Error等级日志
+ * 由于动态获取Log,效率较低,建议在非频繁调用的情况下使用!! + * + * @param format 格式文本,{} 代表变量 + * @param arguments 变量对应的参数 + */ + public static void error(String format, Object... arguments) { + error(LogFactory.get(CallerUtil.getCallerCaller()), format, arguments); + } + + /** + * Error等级日志
+ * 由于动态获取Log,效率较低,建议在非频繁调用的情况下使用!! + * + * @param e 需在日志中堆栈打印的异常 + * @param format 格式文本,{} 代表变量 + * @param arguments 变量对应的参数 + */ + public static void error(Throwable e, String format, Object... arguments) { + error(LogFactory.get(CallerUtil.getCallerCaller()), e, format, arguments); + } + + /** + * Error等级日志
+ * + * @param log 日志对象 + * @param e 需在日志中堆栈打印的异常 + */ + public static void error(Log log, Throwable e) { + error(log, e, e.getMessage()); + } + + /** + * Error等级日志
+ * + * @param log 日志对象 + * @param format 格式文本,{} 代表变量 + * @param arguments 变量对应的参数 + */ + public static void error(Log log, String format, Object... arguments) { + error(log, null, format, arguments); + } + + /** + * Error等级日志
+ * + * @param log 日志对象 + * @param e 需在日志中堆栈打印的异常 + * @param format 格式文本,{} 代表变量 + * @param arguments 变量对应的参数 + */ + public static void error(Log log, Throwable e, String format, Object... arguments) { + log.error(FQCN, e, format, arguments); + } + + // ------------------------ Log + /** + * 打印日志
+ * + * @param level 日志级别 + * @param t 需在日志中堆栈打印的异常 + * @param format 格式文本,{} 代表变量 + * @param arguments 变量对应的参数 + */ + public static void log(Level level, Throwable t, String format, Object... arguments) { + LogFactory.get(CallerUtil.getCallerCaller()).log(FQCN, level, t, format, arguments); + } + + // ----------------------------------------------------------- Log method end + + /** + * 获得Log + * + * @param clazz 日志发出的类 + * @return Log + */ + public static Log get(Class clazz) { + return LogFactory.get(clazz); + } + + /** + * 获得Log + * + * @param name 自定义的日志发出者名称 + * @return Log + */ + public static Log get(String name) { + return LogFactory.get(name); + } + + /** + * @return 获得日志,自动判定日志发出者 + */ + public static Log get() { + return LogFactory.get(CallerUtil.getCallerCaller()); + } +} diff --git a/hutool-log/src/main/java/cn/hutool/log/dialect/commons/ApacheCommonsLog.java b/hutool-log/src/main/java/cn/hutool/log/dialect/commons/ApacheCommonsLog.java new file mode 100644 index 000000000..9262e9895 --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/dialect/commons/ApacheCommonsLog.java @@ -0,0 +1,146 @@ +package cn.hutool.log.dialect.commons; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.log.AbstractLog; +import cn.hutool.log.level.Level; + +/** + * Apache Commons Logging + * @author Looly + * + */ +public class ApacheCommonsLog extends AbstractLog { + private static final long serialVersionUID = -6843151523380063975L; + + private final transient Log logger; + private final String name; + + // ------------------------------------------------------------------------- Constructor + public ApacheCommonsLog(Log logger, String name) { + this.logger = logger; + this.name = name; + } + + public ApacheCommonsLog(Class clazz) { + this(LogFactory.getLog(clazz), null == clazz ? StrUtil.NULL : clazz.getName()); + } + + public ApacheCommonsLog(String name) { + this(LogFactory.getLog(name), name); + } + + @Override + public String getName() { + return this.name; + } + + // ------------------------------------------------------------------------- Trace + @Override + public boolean isTraceEnabled() { + return logger.isTraceEnabled(); + } + + @Override + public void trace(String fqcn, Throwable t, String format, Object... arguments) { + // fqcn此处无效 + if(isTraceEnabled()){ + logger.trace(StrUtil.format(format, arguments), t); + } + } + + // ------------------------------------------------------------------------- Debug + @Override + public boolean isDebugEnabled() { + return logger.isDebugEnabled(); + } + + @Override + public void debug(String fqcn, Throwable t, String format, Object... arguments) { + // fqcn此处无效 + if(isDebugEnabled()){ + logger.debug(StrUtil.format(format, arguments), t); + } + } + + // ------------------------------------------------------------------------- Info + @Override + public boolean isInfoEnabled() { + return logger.isInfoEnabled(); + } + + @Override + public void info(String fqcn, Throwable t, String format, Object... arguments) { + // fqcn此处无效 + if(isInfoEnabled()){ + logger.info(StrUtil.format(format, arguments), t); + } + } + + // ------------------------------------------------------------------------- Warn + @Override + public boolean isWarnEnabled() { + return logger.isWarnEnabled(); + } + + @Override + public void warn(String format, Object... arguments) { + if(isWarnEnabled()){ + logger.warn(StrUtil.format(format, arguments)); + } + } + + @Override + public void warn(Throwable t, String format, Object... arguments) { + } + + @Override + public void warn(String fqcn, Throwable t, String format, Object... arguments) { + // fqcn此处无效 + if(isWarnEnabled()){ + logger.warn(StrUtil.format(format, arguments), t); + } + } + + // ------------------------------------------------------------------------- Error + @Override + public boolean isErrorEnabled() { + return logger.isErrorEnabled(); + } + + @Override + public void error(String fqcn, Throwable t, String format, Object... arguments) { + // fqcn此处无效 + if(isErrorEnabled()){ + logger.warn(StrUtil.format(format, arguments), t); + } + + } + + // ------------------------------------------------------------------------- Log + @Override + public void log(String fqcn, Level level, Throwable t, String format, Object... arguments) { + switch (level) { + case TRACE: + trace(t, format, arguments); + break; + case DEBUG: + debug(t, format, arguments); + break; + case INFO: + info(t, format, arguments); + break; + case WARN: + warn(t, format, arguments); + break; + case ERROR: + error(t, format, arguments); + break; + default: + throw new Error(StrUtil.format("Can not identify level: {}", level)); + } + } + // ------------------------------------------------------------------------- Private method +} diff --git a/hutool-log/src/main/java/cn/hutool/log/dialect/commons/ApacheCommonsLog4JLog.java b/hutool-log/src/main/java/cn/hutool/log/dialect/commons/ApacheCommonsLog4JLog.java new file mode 100644 index 000000000..92c84a681 --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/dialect/commons/ApacheCommonsLog4JLog.java @@ -0,0 +1,28 @@ +package cn.hutool.log.dialect.commons; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.impl.Log4JLogger; + +import cn.hutool.log.dialect.log4j.Log4jLog; + +/** + * Apache Commons Logging for Log4j + * @author Looly + * + */ +public class ApacheCommonsLog4JLog extends Log4jLog { + private static final long serialVersionUID = -6843151523380063975L; + + // ------------------------------------------------------------------------- Constructor + public ApacheCommonsLog4JLog(Log logger) { + super(((Log4JLogger) logger).getLogger()); + } + + public ApacheCommonsLog4JLog(Class clazz) { + super(clazz); + } + + public ApacheCommonsLog4JLog(String name) { + super(name); + } +} diff --git a/hutool-log/src/main/java/cn/hutool/log/dialect/commons/ApacheCommonsLogFactory.java b/hutool-log/src/main/java/cn/hutool/log/dialect/commons/ApacheCommonsLogFactory.java new file mode 100644 index 000000000..04a642b48 --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/dialect/commons/ApacheCommonsLogFactory.java @@ -0,0 +1,42 @@ +package cn.hutool.log.dialect.commons; + +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; + +/** + * Apache Commons Logging + * @author Looly + * + */ +public class ApacheCommonsLogFactory extends LogFactory{ + + public ApacheCommonsLogFactory() { + super("Apache Common Logging"); + checkLogExist(org.apache.commons.logging.LogFactory.class); + } + + @Override + public Log createLog(String name) { + try { + return new ApacheCommonsLog4JLog(name); + } catch (Exception e) { + return new ApacheCommonsLog(name); + } + } + + @Override + public Log createLog(Class clazz) { + try { + return new ApacheCommonsLog4JLog(clazz); + } catch (Exception e) { + return new ApacheCommonsLog(clazz); + } + } + + @Override + protected void checkLogExist(Class logClassName) { + super.checkLogExist(logClassName); + //Commons Logging在调用getLog时才检查是否有日志实现,在此提前检查,如果没有实现则跳过之 + getLog(ApacheCommonsLogFactory.class); + } +} diff --git a/hutool-log/src/main/java/cn/hutool/log/dialect/commons/package-info.java b/hutool-log/src/main/java/cn/hutool/log/dialect/commons/package-info.java new file mode 100644 index 000000000..f0eecb5b8 --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/dialect/commons/package-info.java @@ -0,0 +1,7 @@ +/** + * Apache-Commons-Logging日志库的实现封装 + * + * @author looly + * + */ +package cn.hutool.log.dialect.commons; \ No newline at end of file diff --git a/hutool-log/src/main/java/cn/hutool/log/dialect/console/ConsoleLog.java b/hutool-log/src/main/java/cn/hutool/log/dialect/console/ConsoleLog.java new file mode 100644 index 000000000..5d059da18 --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/dialect/console/ConsoleLog.java @@ -0,0 +1,140 @@ +package cn.hutool.log.dialect.console; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.Console; +import cn.hutool.core.lang.Dict; +import cn.hutool.core.util.StrUtil; +import cn.hutool.log.AbstractLog; +import cn.hutool.log.level.Level; + +/** + * 利用System.out.println()打印日志 + * @author Looly + * + */ +public class ConsoleLog extends AbstractLog { + private static final long serialVersionUID = -6843151523380063975L; + + private static String logFormat = "[{date}] [{level}] {name}: {msg}"; + private static Level currentLevel = Level.DEBUG; + + private String name; + + //------------------------------------------------------------------------- Constructor + /** + * 构造 + * + * @param clazz 类 + */ + public ConsoleLog(Class clazz) { + this.name = (null == clazz) ? StrUtil.NULL : clazz.getName(); + } + + /** + * 构造 + * + * @param name 类名 + */ + public ConsoleLog(String name) { + this.name = name; + } + + @Override + public String getName() { + return this.name; + } + + /** + * 设置自定义的日志显示级别 + * @param customLevel 自定义级别 + * @since 4.1.10 + */ + public static void setLevel(Level customLevel) { + Assert.notNull(customLevel); + currentLevel = customLevel; + } + + //------------------------------------------------------------------------- Trace + @Override + public boolean isTraceEnabled() { + return isEnabled(Level.TRACE); + } + + @Override + public void trace(String fqcn, Throwable t, String format, Object... arguments) { + log(fqcn, Level.TRACE, t, format, arguments); + } + //------------------------------------------------------------------------- Debug + @Override + public boolean isDebugEnabled() { + return isEnabled(Level.DEBUG); + } + + @Override + public void debug(String fqcn, Throwable t, String format, Object... arguments) { + log(fqcn, Level.DEBUG, t, format, arguments); + } + + //------------------------------------------------------------------------- Info + @Override + public boolean isInfoEnabled() { + return isEnabled(Level.INFO); + } + + @Override + public void info(String fqcn, Throwable t, String format, Object... arguments) { + log(fqcn, Level.INFO, t, format, arguments); + } + + //------------------------------------------------------------------------- Warn + @Override + public boolean isWarnEnabled() { + return isEnabled(Level.WARN); + } + + @Override + public void warn(String fqcn, Throwable t, String format, Object... arguments) { + log(fqcn, Level.WARN, t, format, arguments); + } + + //------------------------------------------------------------------------- Error + @Override + public boolean isErrorEnabled() { + return isEnabled(Level.ERROR); + } + + @Override + public void error(String fqcn, Throwable t, String format, Object... arguments) { + log(fqcn, Level.ERROR, t, format, arguments); + } + + //------------------------------------------------------------------------- Log + @Override + public void log(String fqcn, Level level, Throwable t, String format, Object... arguments) { + // fqcn 无效 + if(false == isEnabled(level)){ + return; + } + + final Dict dict = Dict.create() + .set("date", DateUtil.now()) + .set("level", level.toString()) + .set("name", this.name) + .set("msg", StrUtil.format(format, arguments)); + + final String logMsg = StrUtil.format(logFormat, dict); + + //WARN以上级别打印至System.err + if(level.ordinal() >= Level.WARN.ordinal()){ + Console.error(t, logMsg); + }else{ + Console.log(t, logMsg); + } + } + + @Override + public boolean isEnabled(Level level) { + return currentLevel.compareTo(level) <= 0; + } +} \ No newline at end of file diff --git a/hutool-log/src/main/java/cn/hutool/log/dialect/console/ConsoleLogFactory.java b/hutool-log/src/main/java/cn/hutool/log/dialect/console/ConsoleLogFactory.java new file mode 100644 index 000000000..ff92bebb3 --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/dialect/console/ConsoleLogFactory.java @@ -0,0 +1,27 @@ +package cn.hutool.log.dialect.console; + +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; + +/** + * 利用System.out.println()打印日志 + * @author Looly + * + */ +public class ConsoleLogFactory extends LogFactory { + + public ConsoleLogFactory() { + super("Hutool Console Logging"); + } + + @Override + public Log createLog(String name) { + return new ConsoleLog(name); + } + + @Override + public Log createLog(Class clazz) { + return new ConsoleLog(clazz); + } + +} diff --git a/hutool-log/src/main/java/cn/hutool/log/dialect/console/package-info.java b/hutool-log/src/main/java/cn/hutool/log/dialect/console/package-info.java new file mode 100644 index 000000000..90529d0b3 --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/dialect/console/package-info.java @@ -0,0 +1,7 @@ +/** + * 控制台输出的实现封装 + * + * @author looly + * + */ +package cn.hutool.log.dialect.console; \ No newline at end of file diff --git a/hutool-log/src/main/java/cn/hutool/log/dialect/jboss/JbossLog.java b/hutool-log/src/main/java/cn/hutool/log/dialect/jboss/JbossLog.java new file mode 100644 index 000000000..c2e708818 --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/dialect/jboss/JbossLog.java @@ -0,0 +1,141 @@ +package cn.hutool.log.dialect.jboss; + +import org.jboss.logging.Logger; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.log.AbstractLog; +import cn.hutool.log.level.Level; + +/** + * Jboss-Logging log. + * + * @author Looly + * + */ +public class JbossLog extends AbstractLog { + private static final long serialVersionUID = -6843151523380063975L; + + private final transient Logger logger; + + // ------------------------------------------------------------------------- Constructor + /** + * 构造 + * + * @param logger {@link Logger} + */ + public JbossLog(Logger logger) { + this.logger = logger; + } + + /** + * 构造 + * + * @param clazz 日志打印所在类 + */ + public JbossLog(Class clazz) { + this((null == clazz) ? StrUtil.NULL : clazz.getName()); + } + + /** + * 构造 + * + * @param name 日志打印所在类名 + */ + public JbossLog(String name) { + this(Logger.getLogger(name)); + } + + @Override + public String getName() { + return logger.getName(); + } + + // ------------------------------------------------------------------------- Trace + @Override + public boolean isTraceEnabled() { + return logger.isTraceEnabled(); + } + + @Override + public void trace(String fqcn, Throwable t, String format, Object... arguments) { + if (isTraceEnabled()) { + logger.trace(fqcn, StrUtil.format(format, arguments), t); + } + } + + // ------------------------------------------------------------------------- Debug + @Override + public boolean isDebugEnabled() { + return logger.isDebugEnabled(); + } + + @Override + public void debug(String fqcn, Throwable t, String format, Object... arguments) { + if (isDebugEnabled()) { + logger.debug(fqcn, StrUtil.format(format, arguments), t); + } + } + + // ------------------------------------------------------------------------- Info + @Override + public boolean isInfoEnabled() { + return logger.isInfoEnabled(); + } + + @Override + public void info(String fqcn, Throwable t, String format, Object... arguments) { + if (isInfoEnabled()) { + logger.info(fqcn, StrUtil.format(format, arguments), t); + } + } + + // ------------------------------------------------------------------------- Warn + @Override + public boolean isWarnEnabled() { + return logger.isEnabled(Logger.Level.WARN); + } + + @Override + public void warn(String fqcn, Throwable t, String format, Object... arguments) { + if (isWarnEnabled()) { + logger.warn(fqcn, StrUtil.format(format, arguments), t); + } + } + + // ------------------------------------------------------------------------- Error + @Override + public boolean isErrorEnabled() { + return logger.isEnabled(Logger.Level.ERROR); + } + + @Override + public void error(String fqcn, Throwable t, String format, Object... arguments) { + if (isErrorEnabled()) { + logger.error(fqcn, StrUtil.format(format, arguments), t); + } + } + + // ------------------------------------------------------------------------- Log + @Override + public void log(String fqcn, Level level, Throwable t, String format, Object... arguments) { + switch (level) { + case TRACE: + trace(fqcn, t, format, arguments); + break; + case DEBUG: + debug(fqcn, t, format, arguments); + break; + case INFO: + info(fqcn, t, format, arguments); + break; + case WARN: + warn(fqcn, t, format, arguments); + break; + case ERROR: + error(fqcn, t, format, arguments); + break; + default: + throw new Error(StrUtil.format("Can not identify level: {}", level)); + } + } +} diff --git a/hutool-log/src/main/java/cn/hutool/log/dialect/jboss/JbossLogFactory.java b/hutool-log/src/main/java/cn/hutool/log/dialect/jboss/JbossLogFactory.java new file mode 100644 index 000000000..e2531e5d0 --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/dialect/jboss/JbossLogFactory.java @@ -0,0 +1,32 @@ +package cn.hutool.log.dialect.jboss; + +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; + +/** + * Jboss-Logging log. + * + * @author Looly + * @since 4.1.21 + */ +public class JbossLogFactory extends LogFactory { + + /** + * 构造 + */ + public JbossLogFactory() { + super("JBoss Logging"); + checkLogExist(org.jboss.logging.Logger.class); + } + + @Override + public Log createLog(String name) { + return new JbossLog(name); + } + + @Override + public Log createLog(Class clazz) { + return new JbossLog(clazz); + } + +} diff --git a/hutool-log/src/main/java/cn/hutool/log/dialect/jboss/package-info.java b/hutool-log/src/main/java/cn/hutool/log/dialect/jboss/package-info.java new file mode 100644 index 000000000..ff549a83f --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/dialect/jboss/package-info.java @@ -0,0 +1,7 @@ +/** + * jboss-logging实现 + * + * @author looly + * + */ +package cn.hutool.log.dialect.jboss; \ No newline at end of file diff --git a/hutool-log/src/main/java/cn/hutool/log/dialect/jdk/JdkLog.java b/hutool-log/src/main/java/cn/hutool/log/dialect/jdk/JdkLog.java new file mode 100644 index 000000000..4dc6434c0 --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/dialect/jdk/JdkLog.java @@ -0,0 +1,166 @@ +package cn.hutool.log.dialect.jdk; + +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.log.AbstractLog; + +/** + * java.util.logging log. + * + * @author Looly + * + */ +public class JdkLog extends AbstractLog { + private static final long serialVersionUID = -6843151523380063975L; + + private final transient Logger logger; + + // ------------------------------------------------------------------------- Constructor + public JdkLog(Logger logger) { + this.logger = logger; + } + + public JdkLog(Class clazz) { + this((null == clazz) ? StrUtil.NULL : clazz.getName()); + } + + public JdkLog(String name) { + this(Logger.getLogger(name)); + } + + @Override + public String getName() { + return logger.getName(); + } + + // ------------------------------------------------------------------------- Trace + @Override + public boolean isTraceEnabled() { + return logger.isLoggable(Level.FINEST); + } + + @Override + public void trace(String fqcn, Throwable t, String format, Object... arguments) { + logIfEnabled(fqcn, Level.FINEST, t, format, arguments); + } + + // ------------------------------------------------------------------------- Debug + @Override + public boolean isDebugEnabled() { + return logger.isLoggable(Level.FINE); + } + + @Override + public void debug(String fqcn, Throwable t, String format, Object... arguments) { + logIfEnabled(fqcn, Level.FINE, t, format, arguments); + } + + // ------------------------------------------------------------------------- Info + @Override + public boolean isInfoEnabled() { + return logger.isLoggable(Level.INFO); + } + + @Override + public void info(String fqcn, Throwable t, String format, Object... arguments) { + logIfEnabled(fqcn, Level.INFO, t, format, arguments); + } + + // ------------------------------------------------------------------------- Warn + @Override + public boolean isWarnEnabled() { + return logger.isLoggable(Level.WARNING); + } + + @Override + public void warn(String fqcn, Throwable t, String format, Object... arguments) { + logIfEnabled(fqcn, Level.WARNING, t, format, arguments); + } + + // ------------------------------------------------------------------------- Error + @Override + public boolean isErrorEnabled() { + return logger.isLoggable(Level.SEVERE); + } + + @Override + public void error(String fqcn, Throwable t, String format, Object... arguments) { + logIfEnabled(fqcn, Level.SEVERE, t, format, arguments); + } + + // ------------------------------------------------------------------------- Log + @Override + public void log(String fqcn, cn.hutool.log.level.Level level, Throwable t, String format, Object... arguments) { + Level jdkLevel; + switch (level) { + case TRACE: + jdkLevel = Level.FINEST; + break; + case DEBUG: + jdkLevel = Level.FINE; + break; + case INFO: + jdkLevel = Level.INFO; + break; + case WARN: + jdkLevel = Level.WARNING; + break; + case ERROR: + jdkLevel = Level.SEVERE; + break; + default: + throw new Error(StrUtil.format("Can not identify level: {}", level)); + } + logIfEnabled(fqcn, jdkLevel, t, format, arguments); + } + + // ------------------------------------------------------------------------- Private method + /** + * 打印对应等级的日志 + * + * @param callerFQCN 调用者的完全限定类名(Fully Qualified Class Name) + * @param level 等级 + * @param throwable 异常对象 + * @param format 消息模板 + * @param arguments 参数 + */ + private void logIfEnabled(String callerFQCN, Level level, Throwable throwable, String format, Object[] arguments){ + if(logger.isLoggable(level)){ + LogRecord record = new LogRecord(level, StrUtil.format(format, arguments)); + record.setLoggerName(getName()); + record.setThrown(throwable); + fillCallerData(callerFQCN, record); + logger.log(record); + } + } + + /** + * 传入调用日志类的信息 + * @param callerFQCN 调用者全限定类名 + * @param superFQCN 调用者父类全限定名 + * @param record The record to update + */ + private static void fillCallerData(String callerFQCN, LogRecord record) { + StackTraceElement[] steArray = Thread.currentThread().getStackTrace(); + + int found = -1; + String className; + for (int i = steArray.length -2; i > -1; i--) { + // 此处初始值为length-2,表示从倒数第二个堆栈开始检查,如果是倒数第一个,那调用者就获取不到 + className = steArray[i].getClassName(); + if (callerFQCN.equals(className)) { + found = i; + break; + } + } + + if (found > -1) { + StackTraceElement ste = steArray[found+1]; + record.setSourceClassName(ste.getClassName()); + record.setSourceMethodName(ste.getMethodName()); + } + } +} diff --git a/hutool-log/src/main/java/cn/hutool/log/dialect/jdk/JdkLogFactory.java b/hutool-log/src/main/java/cn/hutool/log/dialect/jdk/JdkLogFactory.java new file mode 100644 index 000000000..a4bf5ae95 --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/dialect/jdk/JdkLogFactory.java @@ -0,0 +1,59 @@ +package cn.hutool.log.dialect.jdk; + +import java.io.InputStream; +import java.util.logging.LogManager; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.lang.Console; +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; + +/** + * JDK日志工厂类 + * java.util.logging log. + * @author Looly + * + */ +public class JdkLogFactory extends LogFactory{ + + public JdkLogFactory() { + super("JDK Logging"); + readConfig(); + } + + @Override + public Log createLog(String name) { + return new JdkLog(name); + } + + @Override + public Log createLog(Class clazz) { + return new JdkLog(clazz); + } + + /** + * 读取ClassPath下的logging.properties配置文件 + */ + private void readConfig() { + //避免循环引用,Log初始化的时候不使用相关工具类 + InputStream in = ResourceUtil.getStreamSafe("logging.properties"); + if(null == in){ + System.err.println("[WARN] Can not find [logging.properties], use [%JRE_HOME%/lib/logging.properties] as default!"); + return; + } + + try { + LogManager.getLogManager().readConfiguration(in); + } catch (Exception e) { + Console.error(e, "Read [logging.properties] from classpath error!"); + try { + LogManager.getLogManager().readConfiguration(); + } catch (Exception e1) { + Console.error(e, "Read [logging.properties] from [%JRE_HOME%/lib/logging.properties] error!"); + } + } finally { + IoUtil.close(in); + } + } +} diff --git a/hutool-log/src/main/java/cn/hutool/log/dialect/jdk/package-info.java b/hutool-log/src/main/java/cn/hutool/log/dialect/jdk/package-info.java new file mode 100644 index 000000000..7baef091b --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/dialect/jdk/package-info.java @@ -0,0 +1,7 @@ +/** + * JDK-Logging的实现封装 + * + * @author looly + * + */ +package cn.hutool.log.dialect.jdk; \ No newline at end of file diff --git a/hutool-log/src/main/java/cn/hutool/log/dialect/log4j/Log4jLog.java b/hutool-log/src/main/java/cn/hutool/log/dialect/log4j/Log4jLog.java new file mode 100644 index 000000000..2159f4083 --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/dialect/log4j/Log4jLog.java @@ -0,0 +1,120 @@ +package cn.hutool.log.dialect.log4j; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.log.AbstractLog; + +/** + * Apache Log4J log.
+ * + * @author Looly + * + */ +public class Log4jLog extends AbstractLog { + private static final long serialVersionUID = -6843151523380063975L; + + private final Logger logger; + + // ------------------------------------------------------------------------- Constructor + public Log4jLog(Logger logger) { + this.logger = logger; + } + + public Log4jLog(Class clazz) { + this((null == clazz) ? StrUtil.NULL : clazz.getName()); + } + + public Log4jLog(String name) { + this(Logger.getLogger(name)); + } + + @Override + public String getName() { + return logger.getName(); + } + + // ------------------------------------------------------------------------- Trace + @Override + public boolean isTraceEnabled() { + return logger.isTraceEnabled(); + } + + @Override + public void trace(String fqcn, Throwable t, String format, Object... arguments) { + log(fqcn, cn.hutool.log.level.Level.TRACE, t, format, arguments); + } + + // ------------------------------------------------------------------------- Debug + @Override + public boolean isDebugEnabled() { + return logger.isDebugEnabled(); + } + + @Override + public void debug(String fqcn, Throwable t, String format, Object... arguments) { + log(fqcn, cn.hutool.log.level.Level.DEBUG, t, format, arguments); + } + // ------------------------------------------------------------------------- Info + @Override + public boolean isInfoEnabled() { + return logger.isInfoEnabled(); + } + + @Override + public void info(String fqcn, Throwable t, String format, Object... arguments) { + log(fqcn, cn.hutool.log.level.Level.INFO, t, format, arguments); + } + + // ------------------------------------------------------------------------- Warn + @Override + public boolean isWarnEnabled() { + return logger.isEnabledFor(Level.WARN); + } + + @Override + public void warn(String fqcn, Throwable t, String format, Object... arguments) { + log(fqcn, cn.hutool.log.level.Level.WARN, t, format, arguments); + } + + // ------------------------------------------------------------------------- Error + @Override + public boolean isErrorEnabled() { + return logger.isEnabledFor(Level.ERROR); + } + + @Override + public void error(String fqcn, Throwable t, String format, Object... arguments) { + log(fqcn, cn.hutool.log.level.Level.ERROR, t, format, arguments); + } + + // ------------------------------------------------------------------------- Log + @Override + public void log(String fqcn, cn.hutool.log.level.Level level, Throwable t, String format, Object... arguments) { + Level log4jLevel; + switch (level) { + case TRACE: + log4jLevel = Level.TRACE; + break; + case DEBUG: + log4jLevel = Level.DEBUG; + break; + case INFO: + log4jLevel = Level.INFO; + break; + case WARN: + log4jLevel = Level.WARN; + break; + case ERROR: + log4jLevel = Level.ERROR; + break; + default: + throw new Error(StrUtil.format("Can not identify level: {}", level)); + } + + if(logger.isEnabledFor(log4jLevel)) { + logger.log(fqcn, log4jLevel, StrUtil.format(format, arguments), t); + } + } +} diff --git a/hutool-log/src/main/java/cn/hutool/log/dialect/log4j/Log4jLogFactory.java b/hutool-log/src/main/java/cn/hutool/log/dialect/log4j/Log4jLogFactory.java new file mode 100644 index 000000000..029a3353b --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/dialect/log4j/Log4jLogFactory.java @@ -0,0 +1,28 @@ +package cn.hutool.log.dialect.log4j; + +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; + +/** + * Apache Log4J log.
+ * @author Looly + * + */ +public class Log4jLogFactory extends LogFactory{ + + public Log4jLogFactory() { + super("Log4j"); + checkLogExist(org.apache.log4j.Logger.class); + } + + @Override + public Log createLog(String name) { + return new Log4jLog(name); + } + + @Override + public Log createLog(Class clazz) { + return new Log4jLog(clazz); + } + +} diff --git a/hutool-log/src/main/java/cn/hutool/log/dialect/log4j/package-info.java b/hutool-log/src/main/java/cn/hutool/log/dialect/log4j/package-info.java new file mode 100644 index 000000000..bc0525807 --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/dialect/log4j/package-info.java @@ -0,0 +1,7 @@ +/** + * Log4j的实现封装 + * + * @author looly + * + */ +package cn.hutool.log.dialect.log4j; \ No newline at end of file diff --git a/hutool-log/src/main/java/cn/hutool/log/dialect/log4j2/Log4j2Log.java b/hutool-log/src/main/java/cn/hutool/log/dialect/log4j2/Log4j2Log.java new file mode 100644 index 000000000..61fac64d7 --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/dialect/log4j2/Log4j2Log.java @@ -0,0 +1,147 @@ +package cn.hutool.log.dialect.log4j2; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.spi.AbstractLogger; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.log.AbstractLog; + +/** + * Apache Log4J 2 log.
+ * + * @author Looly + * + */ +public class Log4j2Log extends AbstractLog { + private static final long serialVersionUID = -6843151523380063975L; + + private final transient Logger logger; + + // ------------------------------------------------------------------------- Constructor + public Log4j2Log(Logger logger) { + this.logger = logger; + } + + public Log4j2Log(Class clazz) { + this(LogManager.getLogger(clazz)); + } + + public Log4j2Log(String name) { + this(LogManager.getLogger(name)); + } + + @Override + public String getName() { + return logger.getName(); + } + + // ------------------------------------------------------------------------- Trace + @Override + public boolean isTraceEnabled() { + return logger.isTraceEnabled(); + } + + @Override + public void trace(String fqcn, Throwable t, String format, Object... arguments) { + logIfEnabled(fqcn, Level.TRACE, t, format, arguments); + } + + // ------------------------------------------------------------------------- Debug + @Override + public boolean isDebugEnabled() { + return logger.isDebugEnabled(); + } + + @Override + public void debug(String format, Object... arguments) { + debug(null, format, arguments); + } + + @Override + public void debug(String fqcn, Throwable t, String format, Object... arguments) { + logIfEnabled(fqcn, Level.DEBUG, t, format, arguments); + } + + // ------------------------------------------------------------------------- Info + @Override + public boolean isInfoEnabled() { + return logger.isInfoEnabled(); + } + + @Override + public void info(String fqcn, Throwable t, String format, Object... arguments) { + logIfEnabled(fqcn, Level.INFO, t, format, arguments); + } + + // ------------------------------------------------------------------------- Warn + @Override + public boolean isWarnEnabled() { + return logger.isWarnEnabled(); + } + + @Override + public void warn(String fqcn, Throwable t, String format, Object... arguments) { + logIfEnabled(fqcn, Level.WARN, t, format, arguments); + } + + // ------------------------------------------------------------------------- Error + @Override + public boolean isErrorEnabled() { + return logger.isErrorEnabled(); + } + + @Override + public void error(String fqcn, Throwable t, String format, Object... arguments) { + logIfEnabled(fqcn, Level.ERROR, t, format, arguments); + } + + // ------------------------------------------------------------------------- Log + @Override + public void log(String fqcn, cn.hutool.log.level.Level level, Throwable t, String format, Object... arguments) { + Level log4j2Level; + switch (level) { + case TRACE: + log4j2Level = Level.TRACE; + break; + case DEBUG: + log4j2Level = Level.DEBUG; + break; + case INFO: + log4j2Level = Level.INFO; + break; + case WARN: + log4j2Level = Level.WARN; + break; + case ERROR: + log4j2Level = Level.ERROR; + break; + default: + throw new Error(StrUtil.format("Can not identify level: {}", level)); + } + logIfEnabled(fqcn, log4j2Level, t, format, arguments); + } + + // ------------------------------------------------------------------------- Private method + /** + * 打印日志
+ * 此方法用于兼容底层日志实现,通过传入当前包装类名,以解决打印日志中行号错误问题 + * + * @param fqcn 完全限定类名(Fully Qualified Class Name),用于纠正定位错误行号 + * @param level 日志级别,使用org.apache.logging.log4j.Level中的常量 + * @param t 异常 + * @param msgTemplate 消息模板 + * @param arguments 参数 + */ + private void logIfEnabled(String fqcn, Level level, Throwable t, String msgTemplate, Object... arguments) { + if(this.logger.isEnabled(level)) { + if(this.logger instanceof AbstractLogger){ + ((AbstractLogger)this.logger).logIfEnabled(fqcn, level, null, StrUtil.format(msgTemplate, arguments), t); + } else { + // FQCN无效 + this.logger.log(level, StrUtil.format(msgTemplate, arguments), t); + } + } + } +} diff --git a/hutool-log/src/main/java/cn/hutool/log/dialect/log4j2/Log4j2LogFactory.java b/hutool-log/src/main/java/cn/hutool/log/dialect/log4j2/Log4j2LogFactory.java new file mode 100644 index 000000000..8848f0459 --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/dialect/log4j2/Log4j2LogFactory.java @@ -0,0 +1,28 @@ +package cn.hutool.log.dialect.log4j2; + +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; + +/** + * Apache Log4J 2 log.
+ * @author Looly + * + */ +public class Log4j2LogFactory extends LogFactory{ + + public Log4j2LogFactory() { + super("Log4j2"); + checkLogExist(org.apache.logging.log4j.LogManager.class); + } + + @Override + public Log createLog(String name) { + return new Log4j2Log(name); + } + + @Override + public Log createLog(Class clazz) { + return new Log4j2Log(clazz); + } + +} diff --git a/hutool-log/src/main/java/cn/hutool/log/dialect/log4j2/package-info.java b/hutool-log/src/main/java/cn/hutool/log/dialect/log4j2/package-info.java new file mode 100644 index 000000000..bc42813eb --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/dialect/log4j2/package-info.java @@ -0,0 +1,7 @@ +/** + * Log4j2的实现封装 + * + * @author looly + * + */ +package cn.hutool.log.dialect.log4j2; \ No newline at end of file diff --git a/hutool-log/src/main/java/cn/hutool/log/dialect/package-info.java b/hutool-log/src/main/java/cn/hutool/log/dialect/package-info.java new file mode 100644 index 000000000..354a127b9 --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/dialect/package-info.java @@ -0,0 +1,7 @@ +/** + * 第三方日志库的实现封装 + * + * @author looly + * + */ +package cn.hutool.log.dialect; \ No newline at end of file diff --git a/hutool-log/src/main/java/cn/hutool/log/dialect/slf4j/Slf4jLog.java b/hutool-log/src/main/java/cn/hutool/log/dialect/slf4j/Slf4jLog.java new file mode 100644 index 000000000..99a02b3bb --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/dialect/slf4j/Slf4jLog.java @@ -0,0 +1,181 @@ +package cn.hutool.log.dialect.slf4j; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.spi.LocationAwareLogger; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.log.AbstractLog; +import cn.hutool.log.level.Level; + +/** + * SLF4J log.
+ * 同样无缝支持 LogBack + * + * @author Looly + * + */ +public class Slf4jLog extends AbstractLog { + private static final long serialVersionUID = -6843151523380063975L; + + private final transient Logger logger; + /** 是否为 LocationAwareLogger ,用于判断是否可以传递FQCN */ + private final boolean isLocationAwareLogger; + + // ------------------------------------------------------------------------- Constructor + public Slf4jLog(Logger logger) { + this.logger = logger; + this.isLocationAwareLogger = (logger instanceof LocationAwareLogger); + } + + public Slf4jLog(Class clazz) { + this(getSlf4jLogger(clazz)); + } + + public Slf4jLog(String name) { + this(LoggerFactory.getLogger(name)); + } + + @Override + public String getName() { + return logger.getName(); + } + + // ------------------------------------------------------------------------- Trace + @Override + public boolean isTraceEnabled() { + return logger.isTraceEnabled(); + } + + @Override + public void trace(String fqcn, Throwable t, String format, Object... arguments) { + if (isTraceEnabled()) { + if(this.isLocationAwareLogger) { + locationAwareLog((LocationAwareLogger)this.logger, fqcn, LocationAwareLogger.TRACE_INT, t, format, arguments); + } else { + logger.trace(StrUtil.format(format, arguments), t); + } + } + } + + // ------------------------------------------------------------------------- Debug + @Override + public boolean isDebugEnabled() { + return logger.isDebugEnabled(); + } + + @Override + public void debug(String fqcn, Throwable t, String format, Object... arguments) { + if (isDebugEnabled()) { + if(this.isLocationAwareLogger) { + locationAwareLog((LocationAwareLogger)this.logger, fqcn, LocationAwareLogger.DEBUG_INT, t, format, arguments); + } else { + logger.debug(StrUtil.format(format, arguments), t); + } + } + } + + // ------------------------------------------------------------------------- Info + @Override + public boolean isInfoEnabled() { + return logger.isInfoEnabled(); + } + + @Override + public void info(String fqcn, Throwable t, String format, Object... arguments) { + if (isInfoEnabled()) { + if(this.isLocationAwareLogger) { + locationAwareLog((LocationAwareLogger)this.logger, fqcn, LocationAwareLogger.INFO_INT, t, format, arguments); + } else { + logger.info(StrUtil.format(format, arguments), t); + } + } + } + + // ------------------------------------------------------------------------- Warn + @Override + public boolean isWarnEnabled() { + return logger.isWarnEnabled(); + } + + @Override + public void warn(String fqcn, Throwable t, String format, Object... arguments) { + if (isWarnEnabled()) { + if(this.isLocationAwareLogger) { + locationAwareLog((LocationAwareLogger)this.logger, fqcn, LocationAwareLogger.WARN_INT, t, format, arguments); + } else { + logger.warn(StrUtil.format(format, arguments), t); + } + } + } + + // ------------------------------------------------------------------------- Error + @Override + public boolean isErrorEnabled() { + return logger.isErrorEnabled(); + } + + @Override + public void error(String fqcn, Throwable t, String format, Object... arguments) { + if (isErrorEnabled()) { + if(this.isLocationAwareLogger) { + locationAwareLog((LocationAwareLogger)this.logger, fqcn, LocationAwareLogger.ERROR_INT, t, format, arguments); + } else { + logger.error(StrUtil.format(format, arguments), t); + } + } + } + + // ------------------------------------------------------------------------- Log + @Override + public void log(String fqcn, Level level, Throwable t, String format, Object... arguments) { + switch (level) { + case TRACE: + trace(fqcn, t, format, arguments); + break; + case DEBUG: + debug(fqcn, t, format, arguments); + break; + case INFO: + info(fqcn, t, format, arguments); + break; + case WARN: + warn(fqcn, t, format, arguments); + break; + case ERROR: + error(fqcn, t, format, arguments); + break; + default: + throw new Error(StrUtil.format("Can not identify level: {}", level)); + } + } + + // -------------------------------------------------------------------------------------------------- Private method + /** + * 打印日志
+ * 此方法用于兼容底层日志实现,通过传入当前包装类名,以解决打印日志中行号错误问题 + * + * @param logger {@link LocationAwareLogger} 实现 + * @param fqcn 完全限定类名(Fully Qualified Class Name),用于纠正定位错误行号 + * @param level_int 日志级别,使用LocationAwareLogger中的常量 + * @param t 异常 + * @param msgTemplate 消息模板 + * @param arguments 参数 + * @return 是否支持 LocationAwareLogger对象,如果不支持需要日志方法调用被包装类的相应方法 + */ + private void locationAwareLog(LocationAwareLogger logger, String fqcn, int level_int, Throwable t, String msgTemplate, Object[] arguments) { + // ((LocationAwareLogger)this.logger).log(null, fqcn, level_int, msgTemplate, arguments, t); + // 由于slf4j-log4j12中此方法的实现存在bug,故在此拼接参数 + logger.log(null, fqcn, level_int, StrUtil.format(msgTemplate, arguments), null, t); + } + + /** + * 获取Slf4j Logger对象 + * + * @param clazz 打印日志所在类,当为{@code null}时使用“null”表示 + * @return {@link Logger} + */ + private static Logger getSlf4jLogger(Class clazz) { + return (null == clazz) ? LoggerFactory.getLogger(StrUtil.EMPTY) : LoggerFactory.getLogger(clazz); + } +} diff --git a/hutool-log/src/main/java/cn/hutool/log/dialect/slf4j/Slf4jLogFactory.java b/hutool-log/src/main/java/cn/hutool/log/dialect/slf4j/Slf4jLogFactory.java new file mode 100644 index 000000000..11969f556 --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/dialect/slf4j/Slf4jLogFactory.java @@ -0,0 +1,75 @@ +package cn.hutool.log.dialect.slf4j; + +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; + +import org.slf4j.LoggerFactory; +import org.slf4j.helpers.NOPLoggerFactory; + +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; + +/** + * SLF4J log.
+ * 同样无缝支持 LogBack + * + * @author Looly + * + */ +public class Slf4jLogFactory extends LogFactory { + + public Slf4jLogFactory() { + this(false); + } + + /** + * 构造 + * + * @param failIfNOP 如果未找到桥接包是否报错 + */ + public Slf4jLogFactory(boolean failIfNOP) { + super("Slf4j"); + checkLogExist(LoggerFactory.class); + if(false == failIfNOP){ + return; + } + + // SFL4J writes it error messages to System.err. Capture them so that the user does not see such a message on + // the console during automatic detection. + final StringBuilder buf = new StringBuilder(); + final PrintStream err = System.err; + try { + System.setErr(new PrintStream(new OutputStream(){ + @Override + public void write(int b) { + buf.append((char) b); + } + }, true, "US-ASCII")); + } catch (UnsupportedEncodingException e) { + throw new Error(e); + } + + try { + if (LoggerFactory.getILoggerFactory() instanceof NOPLoggerFactory) { + throw new NoClassDefFoundError(buf.toString()); + } else { + err.print(buf); + err.flush(); + } + } finally { + System.setErr(err); + } + } + + @Override + public Log createLog(String name) { + return new Slf4jLog(name); + } + + @Override + public Log createLog(Class clazz) { + return new Slf4jLog(clazz); + } + +} diff --git a/hutool-log/src/main/java/cn/hutool/log/dialect/slf4j/package-info.java b/hutool-log/src/main/java/cn/hutool/log/dialect/slf4j/package-info.java new file mode 100644 index 000000000..74cf95d17 --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/dialect/slf4j/package-info.java @@ -0,0 +1,7 @@ +/** + * Slf4j的实现封装 + * + * @author looly + * + */ +package cn.hutool.log.dialect.slf4j; \ No newline at end of file diff --git a/hutool-log/src/main/java/cn/hutool/log/dialect/tinylog/TinyLog.java b/hutool-log/src/main/java/cn/hutool/log/dialect/tinylog/TinyLog.java new file mode 100644 index 000000000..9c82d3a32 --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/dialect/tinylog/TinyLog.java @@ -0,0 +1,170 @@ +package cn.hutool.log.dialect.tinylog; + +import org.pmw.tinylog.Level; +import org.pmw.tinylog.LogEntryForwarder; +import org.pmw.tinylog.Logger; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.log.AbstractLog; + +/** + * tinylog log.
+ * + * @author Looly + * + */ +public class TinyLog extends AbstractLog { + private static final long serialVersionUID = -4848042277045993735L; + + /** 堆栈增加层数,因为封装因此多了两层,此值用于正确获取当前类名 */ + private static final int DEPTH = 4; + + private int level; + private String name; + + // ------------------------------------------------------------------------- Constructor + public TinyLog(Class clazz) { + this(null == clazz ? StrUtil.NULL : clazz.getName()); + } + + public TinyLog(String name) { + this.name = name; + this.level = Logger.getLevel(name).ordinal(); + } + + @Override + public String getName() { + return this.name; + } + + // ------------------------------------------------------------------------- Trace + @Override + public boolean isTraceEnabled() { + return this.level <= Level.TRACE.ordinal(); + } + + @Override + public void trace(String fqcn, Throwable t, String format, Object... arguments) { + logIfEnabled(fqcn, Level.TRACE, t, format, arguments); + } + + // ------------------------------------------------------------------------- Debug + @Override + public boolean isDebugEnabled() { + return this.level <= Level.DEBUG.ordinal(); + } + + @Override + public void debug(String fqcn, Throwable t, String format, Object... arguments) { + logIfEnabled(fqcn, Level.DEBUG, t, format, arguments); + } + // ------------------------------------------------------------------------- Info + @Override + public boolean isInfoEnabled() { + return this.level <= Level.INFO.ordinal(); + } + + @Override + public void info(String fqcn, Throwable t, String format, Object... arguments) { + logIfEnabled(fqcn, Level.INFO, t, format, arguments); + } + + // ------------------------------------------------------------------------- Warn + @Override + public boolean isWarnEnabled() { + return this.level <= org.pmw.tinylog.Level.WARNING.ordinal(); + } + + @Override + public void warn(String fqcn, Throwable t, String format, Object... arguments) { + logIfEnabled(fqcn, Level.WARNING, t, format, arguments); + } + + // ------------------------------------------------------------------------- Error + @Override + public boolean isErrorEnabled() { + return this.level <= Level.ERROR.ordinal(); + } + + @Override + public void error(String fqcn, Throwable t, String format, Object... arguments) { + logIfEnabled(fqcn, Level.ERROR, t, format, arguments); + } + + // ------------------------------------------------------------------------- Log + @Override + public void log(String fqcn, cn.hutool.log.level.Level level, Throwable t, String format, Object... arguments) { + logIfEnabled(fqcn, toTinyLevel(level), t, format, arguments); + } + + @Override + public boolean isEnabled(cn.hutool.log.level.Level level) { + return this.level <= toTinyLevel(level).ordinal(); + } + + /** + * 在对应日志级别打开情况下打印日志 + * @param fqcn 完全限定类名(Fully Qualified Class Name),用于定位日志位置 + * @param level 日志级别 + * @param t 异常,null则检查最后一个参数是否为Throwable类型,是则取之,否则不打印堆栈 + * @param format 日志消息模板 + * @param arguments 日志消息参数 + */ + private void logIfEnabled(String fqcn, Level level, Throwable t, String format, Object... arguments) { + // fqcn 无效 + if(null == t){ + t = getLastArgumentIfThrowable(arguments); + } + LogEntryForwarder.forward(DEPTH, level, t, format, arguments); + } + + /** + * 将Hutool的Level等级转换为Tinylog的Level等级 + * + * @param level Hutool的Level等级 + * @return Tinylog的Level + * @since 4.0.3 + */ + private Level toTinyLevel(cn.hutool.log.level.Level level) { + Level tinyLevel; + switch (level) { + case TRACE: + tinyLevel = Level.TRACE; + break; + case DEBUG: + tinyLevel = Level.DEBUG; + break; + case INFO: + tinyLevel = Level.INFO; + break; + case WARN: + tinyLevel = Level.WARNING; + break; + case ERROR: + tinyLevel = Level.ERROR; + break; + case OFF: + tinyLevel = Level.OFF; + break; + default: + throw new Error(StrUtil.format("Can not identify level: {}", level)); + } + return tinyLevel; + } + + /** + * 如果最后一个参数为异常参数,则获取之,否则返回null + * + * @param arguments 参数 + * @return 最后一个异常参数 + * @since 4.0.3 + */ + private static Throwable getLastArgumentIfThrowable(Object... arguments) { + if (ArrayUtil.isNotEmpty(arguments) && arguments[arguments.length - 1] instanceof Throwable) { + return (Throwable) arguments[arguments.length - 1]; + } else { + return null; + } + } +} diff --git a/hutool-log/src/main/java/cn/hutool/log/dialect/tinylog/TinyLogFactory.java b/hutool-log/src/main/java/cn/hutool/log/dialect/tinylog/TinyLogFactory.java new file mode 100644 index 000000000..063caa624 --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/dialect/tinylog/TinyLogFactory.java @@ -0,0 +1,32 @@ +package cn.hutool.log.dialect.tinylog; + +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; + +/** + * TinyLog log.
+ * + * @author Looly + * + */ +public class TinyLogFactory extends LogFactory { + + /** + * 构造 + */ + public TinyLogFactory() { + super("TinyLog"); + checkLogExist(org.pmw.tinylog.Logger.class); + } + + @Override + public Log createLog(String name) { + return new TinyLog(name); + } + + @Override + public Log createLog(Class clazz) { + return new TinyLog(clazz); + } + +} diff --git a/hutool-log/src/main/java/cn/hutool/log/dialect/tinylog/package-info.java b/hutool-log/src/main/java/cn/hutool/log/dialect/tinylog/package-info.java new file mode 100644 index 000000000..e9ed843c9 --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/dialect/tinylog/package-info.java @@ -0,0 +1,7 @@ +/** + * TinyLog的实现封装 + * + * @author looly + * + */ +package cn.hutool.log.dialect.tinylog; \ No newline at end of file diff --git a/hutool-log/src/main/java/cn/hutool/log/level/DebugLog.java b/hutool-log/src/main/java/cn/hutool/log/level/DebugLog.java new file mode 100644 index 000000000..db95a3ff7 --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/level/DebugLog.java @@ -0,0 +1,47 @@ +package cn.hutool.log.level; + +/** + * DEBUG级别日志接口 + * @author Looly + * + */ +public interface DebugLog { + /** + * @return DEBUG 等级是否开启 + */ + boolean isDebugEnabled(); + + /** + * 打印 DEBUG 等级的日志 + * + * @param t 错误对象 + */ + void debug(Throwable t); + + /** + * 打印 DEBUG 等级的日志 + * + * @param format 消息模板 + * @param arguments 参数 + */ + void debug(String format, Object... arguments); + + /** + * 打印 DEBUG 等级的日志 + * + * @param t 错误对象 + * @param format 消息模板 + * @param arguments 参数 + */ + void debug(Throwable t, String format, Object... arguments); + + /** + * 打印 DEBUG 等级的日志 + * + * @param fqcn 完全限定类名(Fully Qualified Class Name),用于定位日志位置 + * @param t 错误对象 + * @param format 消息模板 + * @param arguments 参数 + */ + void debug(String fqcn, Throwable t, String format, Object... arguments); +} diff --git a/hutool-log/src/main/java/cn/hutool/log/level/ErrorLog.java b/hutool-log/src/main/java/cn/hutool/log/level/ErrorLog.java new file mode 100644 index 000000000..2659d6247 --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/level/ErrorLog.java @@ -0,0 +1,47 @@ +package cn.hutool.log.level; + +/** + * ERROR级别日志接口 + * @author Looly + * + */ +public interface ErrorLog { + /** + * @return ERROR 等级是否开启 + */ + boolean isErrorEnabled(); + + /** + * 打印 ERROR 等级的日志 + * + * @param t 错误对象 + */ + void error(Throwable t); + + /** + * 打印 ERROR 等级的日志 + * + * @param format 消息模板 + * @param arguments 参数 + */ + void error(String format, Object... arguments); + + /** + * 打印 ERROR 等级的日志 + * + * @param t 错误对象 + * @param format 消息模板 + * @param arguments 参数 + */ + void error(Throwable t, String format, Object... arguments); + + /** + * 打印 ERROR 等级的日志 + * + * @param fqcn 完全限定类名(Fully Qualified Class Name),用于定位日志位置 + * @param t 错误对象 + * @param format 消息模板 + * @param arguments 参数 + */ + void error(String fqcn, Throwable t, String format, Object... arguments); +} diff --git a/hutool-log/src/main/java/cn/hutool/log/level/InfoLog.java b/hutool-log/src/main/java/cn/hutool/log/level/InfoLog.java new file mode 100644 index 000000000..7edd33679 --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/level/InfoLog.java @@ -0,0 +1,47 @@ +package cn.hutool.log.level; + +/** + * INFO级别日志接口 + * @author Looly + * + */ +public interface InfoLog { + /** + * @return INFO 等级是否开启 + */ + boolean isInfoEnabled(); + + /** + * 打印 INFO 等级的日志 + * + * @param t 错误对象 + */ + void info(Throwable t); + + /** + * 打印 INFO 等级的日志 + * + * @param format 消息模板 + * @param arguments 参数 + */ + void info(String format, Object... arguments); + + /** + * 打印 INFO 等级的日志 + * + * @param t 错误对象 + * @param format 消息模板 + * @param arguments 参数 + */ + void info(Throwable t, String format, Object... arguments); + + /** + * 打印 INFO 等级的日志 + * + * @param fqcn 完全限定类名(Fully Qualified Class Name),用于定位日志位置 + * @param t 错误对象 + * @param format 消息模板 + * @param arguments 参数 + */ + void info(String fqcn, Throwable t, String format, Object... arguments); +} diff --git a/hutool-log/src/main/java/cn/hutool/log/level/Level.java b/hutool-log/src/main/java/cn/hutool/log/level/Level.java new file mode 100644 index 000000000..5c0d9b4f9 --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/level/Level.java @@ -0,0 +1,41 @@ +package cn.hutool.log.level; + +/** + * 日志等级 + * @author Looly + * + */ +public enum Level{ + /** + * 'ALL' log level. + */ + ALL, + /** + * 'TRACE' log level. + */ + TRACE, + /** + * 'DEBUG' log level. + */ + DEBUG, + /** + * 'INFO' log level. + */ + INFO, + /** + * 'WARN' log level. + */ + WARN, + /** + * 'ERROR' log level. + */ + ERROR, + /** + * 'FATAL' log level. + */ + FATAL, + /** + * 'OFF' log. + */ + OFF +} diff --git a/hutool-log/src/main/java/cn/hutool/log/level/TraceLog.java b/hutool-log/src/main/java/cn/hutool/log/level/TraceLog.java new file mode 100644 index 000000000..bb1a14ff4 --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/level/TraceLog.java @@ -0,0 +1,47 @@ +package cn.hutool.log.level; + +/** + * TRACE级别日志接口 + * @author Looly + * + */ +public interface TraceLog { + /** + * @return TRACE 等级是否开启 + */ + boolean isTraceEnabled(); + + /** + * 打印 TRACE 等级的日志 + * + * @param t 错误对象 + */ + void trace(Throwable t); + + /** + * 打印 TRACE 等级的日志 + * + * @param format 消息模板 + * @param arguments 参数 + */ + void trace(String format, Object... arguments); + + /** + * 打印 TRACE 等级的日志 + * + * @param t 错误对象 + * @param format 消息模板 + * @param arguments 参数 + */ + void trace(Throwable t, String format, Object... arguments); + + /** + * 打印 TRACE 等级的日志 + * + * @param fqcn 完全限定类名(Fully Qualified Class Name),用于定位日志位置 + * @param t 错误对象 + * @param format 消息模板 + * @param arguments 参数 + */ + void trace(String fqcn, Throwable t, String format, Object... arguments); +} diff --git a/hutool-log/src/main/java/cn/hutool/log/level/WarnLog.java b/hutool-log/src/main/java/cn/hutool/log/level/WarnLog.java new file mode 100644 index 000000000..ba1778f3d --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/level/WarnLog.java @@ -0,0 +1,47 @@ +package cn.hutool.log.level; + +/** + * WARN级别日志接口 + * @author Looly + * + */ +public interface WarnLog { + /** + * @return WARN 等级是否开启 + */ + boolean isWarnEnabled(); + + /** + * 打印 WARN 等级的日志 + * + * @param t 错误对象 + */ + void warn(Throwable t); + + /** + * 打印 WARN 等级的日志 + * + * @param format 消息模板 + * @param arguments 参数 + */ + void warn(String format, Object... arguments); + + /** + * 打印 WARN 等级的日志 + * + * @param t 错误对象 + * @param format 消息模板 + * @param arguments 参数 + */ + void warn(Throwable t, String format, Object... arguments); + + /** + * 打印 WARN 等级的日志 + * + * @param fqcn 完全限定类名(Fully Qualified Class Name),用于定位日志位置 + * @param t 错误对象 + * @param format 消息模板 + * @param arguments 参数 + */ + void warn(String fqcn, Throwable t, String format, Object... arguments); +} diff --git a/hutool-log/src/main/java/cn/hutool/log/level/package-info.java b/hutool-log/src/main/java/cn/hutool/log/level/package-info.java new file mode 100644 index 000000000..cc15b0891 --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/level/package-info.java @@ -0,0 +1,7 @@ +/** + * 按照日志级别定义的日志打印接口定义 + * + * @author looly + * + */ +package cn.hutool.log.level; \ No newline at end of file diff --git a/hutool-log/src/main/java/cn/hutool/log/package-info.java b/hutool-log/src/main/java/cn/hutool/log/package-info.java new file mode 100644 index 000000000..205401c98 --- /dev/null +++ b/hutool-log/src/main/java/cn/hutool/log/package-info.java @@ -0,0 +1,7 @@ +/** + * Hutool-log只是一个日志的通用门面,功能类似于Slf4j。根据加入ClassPath中的jar包动态检测日志实现的方式,使日志使用个更加便利灵活。 + * + * @author looly + * + */ +package cn.hutool.log; \ No newline at end of file diff --git a/hutool-log/src/test/java/cn/hutool/log/test/CustomLogTest.java b/hutool-log/src/test/java/cn/hutool/log/test/CustomLogTest.java new file mode 100644 index 000000000..4eb5089cf --- /dev/null +++ b/hutool-log/src/test/java/cn/hutool/log/test/CustomLogTest.java @@ -0,0 +1,105 @@ +package cn.hutool.log.test; + +import org.junit.Test; + +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import cn.hutool.log.dialect.commons.ApacheCommonsLogFactory; +import cn.hutool.log.dialect.console.ConsoleLogFactory; +import cn.hutool.log.dialect.jboss.JbossLogFactory; +import cn.hutool.log.dialect.jdk.JdkLogFactory; +import cn.hutool.log.dialect.log4j.Log4jLogFactory; +import cn.hutool.log.dialect.log4j2.Log4j2LogFactory; +import cn.hutool.log.dialect.slf4j.Slf4jLogFactory; +import cn.hutool.log.dialect.tinylog.TinyLogFactory; + +/** + * 日志门面单元测试 + * @author Looly + * + */ +public class CustomLogTest { + + private static final String LINE = "----------------------------------------------------------------------"; + + @Test + public void consoleLogTest(){ + LogFactory factory = new ConsoleLogFactory(); + LogFactory.setCurrentLogFactory(factory); + Log log = LogFactory.get(); + + log.info(null); + log.info("This is custom '{}' log\n{}", factory.getName(), LINE); + } + + @Test + public void commonsLogTest(){ + LogFactory factory = new ApacheCommonsLogFactory(); + LogFactory.setCurrentLogFactory(factory); + Log log = LogFactory.get(); + + log.info(null); + log.info("This is custom '{}' log\n{}", factory.getName(), LINE); + } + + @Test + public void tinyLogTest(){ + LogFactory factory = new TinyLogFactory(); + LogFactory.setCurrentLogFactory(factory); + Log log = LogFactory.get(); + + log.info(null); + log.info("This is custom '{}' log\n{}", factory.getName(), LINE); + } + + @Test + public void log4j2LogTest(){ + LogFactory factory = new Log4j2LogFactory(); + LogFactory.setCurrentLogFactory(factory); + Log log = LogFactory.get(); + + log.info(null); + log.info("This is custom '{}' log\n{}", factory.getName(), LINE); + } + + @Test + public void log4jLogTest(){ + LogFactory factory = new Log4jLogFactory(); + LogFactory.setCurrentLogFactory(factory); + Log log = LogFactory.get(); + + log.info(null); + log.info("This is custom '{}' log\n{}", factory.getName(), LINE); + + } + + @Test + public void jbossLogTest(){ + LogFactory factory = new JbossLogFactory(); + LogFactory.setCurrentLogFactory(factory); + Log log = LogFactory.get(); + + log.info(null); + log.info("This is custom '{}' log\n{}", factory.getName(), LINE); + } + + @Test + public void jdkLogTest(){ + LogFactory factory = new JdkLogFactory(); + LogFactory.setCurrentLogFactory(factory); + Log log = LogFactory.get(); + + log.info(null); + log.info("This is custom '{}' log\n{}", factory.getName(), LINE); + } + + @Test + public void slf4jTest(){ + LogFactory factory = new Slf4jLogFactory(); + LogFactory.setCurrentLogFactory(factory); + Log log = LogFactory.get(); + + log.info(null); + log.info("This is custom '{}' log\n{}", factory.getName(), LINE); + } +} diff --git a/hutool-log/src/test/java/cn/hutool/log/test/LogTest.java b/hutool-log/src/test/java/cn/hutool/log/test/LogTest.java new file mode 100644 index 000000000..1f85d077f --- /dev/null +++ b/hutool-log/src/test/java/cn/hutool/log/test/LogTest.java @@ -0,0 +1,40 @@ +package cn.hutool.log.test; + +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import cn.hutool.log.level.Level; + +/** + * 日志门面单元测试 + * @author Looly + * + */ +public class LogTest { + + @Test + public void logTest(){ + Log log = LogFactory.get(); + + // 自动选择日志实现 + log.debug("This is {} log", Level.DEBUG); + log.info("This is {} log", Level.INFO); + log.warn("This is {} log", Level.WARN); + +// Exception e = new Exception("test Exception"); +// log.error(e, "This is {} log", Level.ERROR); + } + + /** + * 兼容slf4j日志消息格式测试,既第二个参数是异常对象时正常输出异常信息 + */ + @Test + @Ignore + public void logWithExceptionTest() { + Log log = LogFactory.get(); + Exception e = new Exception("test Exception"); + log.error("我是错误消息", e); + } +} diff --git a/hutool-log/src/test/java/cn/hutool/log/test/StaticLogTest.java b/hutool-log/src/test/java/cn/hutool/log/test/StaticLogTest.java new file mode 100644 index 000000000..16be05e2f --- /dev/null +++ b/hutool-log/src/test/java/cn/hutool/log/test/StaticLogTest.java @@ -0,0 +1,13 @@ +package cn.hutool.log.test; + +import org.junit.Test; + +import cn.hutool.log.StaticLog; + +public class StaticLogTest { + @Test + public void test() { + StaticLog.debug("This is static {} log", "debug"); + StaticLog.info("This is static {} log", "info"); + } +} diff --git a/hutool-log/src/test/resources/example/log4j2.xml b/hutool-log/src/test/resources/example/log4j2.xml new file mode 100644 index 000000000..aff4e7818 --- /dev/null +++ b/hutool-log/src/test/resources/example/log4j2.xml @@ -0,0 +1,72 @@ + + + + + + + + + /home/logs + + + + + + + + + + + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %class{36}:%L %M -- %msg%xEx%n + + + + + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %class{36}:%L %M -- %msg%xEx%n + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/hutool-log/src/test/resources/log4j.properties b/hutool-log/src/test/resources/log4j.properties new file mode 100644 index 000000000..6c4844480 --- /dev/null +++ b/hutool-log/src/test/resources/log4j.properties @@ -0,0 +1,6 @@ +log4j.rootLogger=debug,STDOUT + +log4j.additivity.org.apache=true +log4j.appender.STDOUT=org.apache.log4j.ConsoleAppender +log4j.appender.STDOUT.layout=org.apache.log4j.PatternLayout +log4j.appender.STDOUT.layout.ConversionPattern=[%d{HH:mm:ss,SSS}][%5p] %c:%L - %m%n \ No newline at end of file diff --git a/hutool-log/src/test/resources/log4j2.xml b/hutool-log/src/test/resources/log4j2.xml new file mode 100644 index 000000000..62fd07ae1 --- /dev/null +++ b/hutool-log/src/test/resources/log4j2.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/hutool-log/src/test/resources/logback.xml b/hutool-log/src/test/resources/logback.xml new file mode 100644 index 000000000..3a29bddf3 --- /dev/null +++ b/hutool-log/src/test/resources/logback.xml @@ -0,0 +1,17 @@ + + + + + + + + + ${format} + + + + + + + + \ No newline at end of file diff --git a/hutool-log/src/test/resources/logging.properties b/hutool-log/src/test/resources/logging.properties new file mode 100644 index 000000000..02e12384c --- /dev/null +++ b/hutool-log/src/test/resources/logging.properties @@ -0,0 +1,39 @@ +#----------------------------------------------------------------------------------------------------------- +# == JDK Logging \u914d\u7f6e\u6587\u4ef6 == +#Level\u7684\u4e94\u4e2a\u7b49\u7ea7 +# SEVERE +# WARNING +# INFO +# CONFIG +# FINE +# FINER +# FINEST +# +#----------------------------------------------------------------------------------------------------------- + +# \u65e5\u5fd7\u683c\u5f0f +#java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n + +#\u6307\u5b9aRoot Logger\u7ea7\u522b +.level= ALL +#\u4e3a Handler \u6307\u5b9a\u9ed8\u8ba4\u7684\u7ea7\u522b\uff08\u9ed8\u8ba4\u4e3a Level.INFO\uff09\u3002 +java.util.logging.ConsoleHandler.level=ALL +# \u6307\u5b9a\u8981\u4f7f\u7528\u7684 Formatter \u7c7b\u7684\u540d\u79f0\uff08\u9ed8\u8ba4\u4e3a java.util.logging.SimpleFormatter\uff09\u3002 +java.util.logging.ConsoleHandler.formatter=logging.formatter.MySimpleFormatter + +# \u4e3a Handler \u6307\u5b9a\u9ed8\u8ba4\u7684\u7ea7\u522b\uff08\u9ed8\u8ba4\u4e3a Level.ALL\uff09\u3002 +java.util.logging.FileHandler.level=ALL +# \u6307\u5b9a\u8981\u4f7f\u7528\u7684 Formatter \u7c7b\u7684\u540d\u79f0\uff08\u9ed8\u8ba4\u4e3a java.util.logging.XMLFormatter\uff09\u3002 +java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter +# \u6307\u5b9a\u8981\u5199\u5165\u5230\u4efb\u610f\u6587\u4ef6\u7684\u8fd1\u4f3c\u6700\u5927\u91cf\uff08\u4ee5\u5b57\u8282\u4e3a\u5355\u4f4d\uff09\u3002\u5982\u679c\u8be5\u6570\u4e3a 0\uff0c\u5219\u6ca1\u6709\u9650\u5236\uff08\u9ed8\u8ba4\u4e3a\u65e0\u9650\u5236\uff09\u3002 +java.util.logging.FileHandler.limit=1024000 +# \u6307\u5b9a\u6709\u591a\u5c11\u8f93\u51fa\u6587\u4ef6\u53c2\u4e0e\u5faa\u73af\uff08\u9ed8\u8ba4\u4e3a 1\uff09\u3002 +java.util.logging.FileHandler.count=1 +# \u4e3a\u751f\u6210\u7684\u8f93\u51fa\u6587\u4ef6\u540d\u79f0\u6307\u5b9a\u4e00\u4e2a\u6a21\u5f0f\u3002\u6709\u5173\u7ec6\u8282\u8bf7\u53c2\u89c1\u4ee5\u4e0b\u5185\u5bb9\uff08\u9ed8\u8ba4\u4e3a "%h/java%u.log"\uff09\u3002 +java.util.logging.FileHandler.pattern=demo.log +# \u6307\u5b9a\u662f\u5426\u5e94\u8be5\u5c06 FileHandler \u8ffd\u52a0\u5230\u4efb\u4f55\u73b0\u6709\u6587\u4ef6\u4e0a\uff08\u9ed8\u8ba4\u4e3a false\uff09\u3002 +java.util.logging.FileHandler.append=true + +# \u6267\u884c\u7684LogHandler\uff0c\u4f7f\u7528\u9017\u53f7\u9694\u5f00 +#handlers= java.util.logging.ConsoleHandler,java.util.logging.FileHandler +handlers= java.util.logging.ConsoleHandler \ No newline at end of file diff --git a/hutool-log/src/test/resources/tinylog.properties b/hutool-log/src/test/resources/tinylog.properties new file mode 100644 index 000000000..6078a18b4 --- /dev/null +++ b/hutool-log/src/test/resources/tinylog.properties @@ -0,0 +1,11 @@ +#----------------------------------------------------------------------------------------------------------- +# == TinyLog \u914d\u7f6e\u6587\u4ef6 == +# \u914d\u7f6e\u89c1\uff1ahttp://www.tinylog.org/configuration#file +#----------------------------------------------------------------------------------------------------------- + +# \u65e5\u5fd7\u5199\u51fa\u65b9\u5f0f\uff1a[console] [file] [jdbc] [logcat] [rollingfile] [sharedfile] [null] +tinylog.writer = console +# \u65e5\u5fd7\u5199\u51fa\u7ea7\u522b\uff1aTRACE < DEBUG < INFO < WARNING < ERROR < OFF +tinylog.level = debug +# \u65e5\u5fd7\u6253\u5370\u683c\u5f0f +tinylog.format = [{date:HH:mm:ss}][{level}] {class}:{line} - {message} \ No newline at end of file diff --git a/hutool-poi/pom.xml b/hutool-poi/pom.xml new file mode 100644 index 000000000..144c22fb5 --- /dev/null +++ b/hutool-poi/pom.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + + jar + + + cn.hutool + hutool-parent + 4.6.2-SNAPSHOT + + + hutool-poi + ${project.artifactId} + Hutool POI工具类(对MS Office操作) + + + + 3.17 + 2.12.0 + + + + + cn.hutool + hutool-core + ${project.parent.version} + + + cn.hutool + hutool-log + ${project.parent.version} + + + + + org.apache.poi + poi-ooxml + ${poi.version} + compile + true + + + xerces + xercesImpl + ${xerces.version} + compile + true + + + diff --git a/hutool-poi/src/main/java/cn/hutool/poi/PoiChecker.java b/hutool-poi/src/main/java/cn/hutool/poi/PoiChecker.java new file mode 100644 index 000000000..747ebfb14 --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/PoiChecker.java @@ -0,0 +1,27 @@ +package cn.hutool.poi; + +import cn.hutool.core.exceptions.DependencyException; +import cn.hutool.core.util.ClassLoaderUtil; + +/** + * POI引入检查器 + * + * @author looly + * @since 4.0.10 + */ +public class PoiChecker { + + /** 没有引入POI的错误消息 */ + public static final String NO_POI_ERROR_MSG = "You need to add dependency of 'poi-ooxml' to your project, and version >= 3.17"; + + /** + * 检查POI包的引入情况 + */ + public static void checkPoiImport() { + try { + Class.forName("org.apache.poi.ss.usermodel.Workbook", false, ClassLoaderUtil.getClassLoader()); + } catch (ClassNotFoundException | NoClassDefFoundError e) { + throw new DependencyException(e, NO_POI_ERROR_MSG); + } + } +} diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/BigExcelWriter.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/BigExcelWriter.java new file mode 100644 index 000000000..f1fbc11ed --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/BigExcelWriter.java @@ -0,0 +1,127 @@ +package cn.hutool.poi.excel; + +import java.io.File; + +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.xssf.streaming.SXSSFWorkbook; + +import cn.hutool.core.io.FileUtil; + +/** + * 大数据量Excel写出 + * + * @author looly + * @since 4.1.13 + */ +public class BigExcelWriter extends ExcelWriter { + + public static final int DEFAULT_WINDOW_SIZE = SXSSFWorkbook.DEFAULT_WINDOW_SIZE; + + // -------------------------------------------------------------------------- Constructor start + /** + * 构造,默认生成xls格式的Excel文件
+ * 此构造不传入写出的Excel文件路径,只能调用{@link #flush(java.io.OutputStream)}方法写出到流
+ * 若写出到文件,还需调用{@link #setDestFile(File)}方法自定义写出的文件,然后调用{@link #flush()}方法写出到文件 + */ + public BigExcelWriter() { + this(DEFAULT_WINDOW_SIZE); + } + + /** + * 构造
+ * 此构造不传入写出的Excel文件路径,只能调用{@link #flush(java.io.OutputStream)}方法写出到流
+ * 若写出到文件,需要调用{@link #flush(File)} 写出到文件 + * + * @param rowAccessWindowSize 在内存中的行数 + */ + public BigExcelWriter(int rowAccessWindowSize) { + this(WorkbookUtil.createSXSSFBook(rowAccessWindowSize), null); + } + + /** + * 构造,默认写出到第一个sheet,第一个sheet名为sheet1 + * + * @param destFilePath 目标文件路径,可以不存在 + */ + public BigExcelWriter(String destFilePath) { + this(destFilePath, null); + } + + /** + * 构造
+ * 此构造不传入写出的Excel文件路径,只能调用{@link #flush(java.io.OutputStream)}方法写出到流
+ * 若写出到文件,需要调用{@link #flush(File)} 写出到文件 + * + * @param rowAccessWindowSize 在内存中的行数 + * @param sheetName sheet名,第一个sheet名并写出到此sheet,例如sheet1 + * @since 4.1.8 + */ + public BigExcelWriter(int rowAccessWindowSize, String sheetName) { + this(WorkbookUtil.createSXSSFBook(rowAccessWindowSize), sheetName); + } + + /** + * 构造 + * + * @param destFilePath 目标文件路径,可以不存在 + * @param sheetName sheet名,第一个sheet名并写出到此sheet,例如sheet1 + */ + public BigExcelWriter(String destFilePath, String sheetName) { + this(FileUtil.file(destFilePath), sheetName); + } + + /** + * 构造,默认写出到第一个sheet,第一个sheet名为sheet1 + * + * @param destFile 目标文件,可以不存在 + */ + public BigExcelWriter(File destFile) { + this(destFile, null); + } + + /** + * 构造 + * + * @param destFile 目标文件,可以不存在 + * @param sheetName sheet名,做为第一个sheet名并写出到此sheet,例如sheet1 + */ + public BigExcelWriter(File destFile, String sheetName) { + this(destFile.exists() ? WorkbookUtil.createSXSSFBook(destFile) : WorkbookUtil.createSXSSFBook(), sheetName); + this.destFile = destFile; + } + + /** + * 构造
+ * 此构造不传入写出的Excel文件路径,只能调用{@link #flush(java.io.OutputStream)}方法写出到流
+ * 若写出到文件,还需调用{@link #setDestFile(File)}方法自定义写出的文件,然后调用{@link #flush()}方法写出到文件 + * + * @param workbook {@link SXSSFWorkbook} + * @param sheetName sheet名,做为第一个sheet名并写出到此sheet,例如sheet1 + */ + public BigExcelWriter(SXSSFWorkbook workbook, String sheetName) { + this(WorkbookUtil.getOrCreateSheet(workbook, sheetName)); + } + + /** + * 构造
+ * 此构造不传入写出的Excel文件路径,只能调用{@link #flush(java.io.OutputStream)}方法写出到流
+ * 若写出到文件,还需调用{@link #setDestFile(File)}方法自定义写出的文件,然后调用{@link #flush()}方法写出到文件 + * + * @param sheet {@link Sheet} + * @since 4.0.6 + */ + public BigExcelWriter(Sheet sheet) { + super(sheet); + } + + // -------------------------------------------------------------------------- Constructor end + + @Override + public void close() { + if (null != this.destFile) { + flush(); + } + ((SXSSFWorkbook) this.workbook).dispose(); + super.closeWithoutFlush(); + } +} diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelBase.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelBase.java new file mode 100644 index 000000000..8e49ee757 --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelBase.java @@ -0,0 +1,320 @@ +package cn.hutool.poi.excel; + +import java.io.Closeable; +import java.util.ArrayList; +import java.util.List; + +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFSheet; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.poi.excel.cell.CellUtil; + +/** + * Excel基础类,用于抽象ExcelWriter和ExcelReader中共用部分的对象和方法 + * + * @param 子类类型,用于返回this + * @author looly + * @since 4.1.4 + */ +public class ExcelBase> implements Closeable { + /** 是否被关闭 */ + protected boolean isClosed; + /** 工作簿 */ + protected Workbook workbook; + /** Excel中对应的Sheet */ + protected Sheet sheet; + + /** + * 构造 + * + * @param sheet Excel中的sheet + */ + public ExcelBase(Sheet sheet) { + Assert.notNull(sheet, "No Sheet provided."); + this.sheet = sheet; + this.workbook = sheet.getWorkbook(); + } + + /** + * 获取Workbook + * + * @return Workbook + */ + public Workbook getWorkbook() { + return this.workbook; + } + + /** + * 返回工作簿表格数 + * + * @return 工作簿表格数 + * @since 4.0.10 + */ + public int getSheetCount() { + return this.workbook.getNumberOfSheets(); + } + + /** + * 获取此工作簿所有Sheet表 + * + * @return sheet表列表 + * @since 4.0.3 + */ + public List getSheets() { + final int totalSheet = getSheetCount(); + final List result = new ArrayList<>(totalSheet); + for (int i = 0; i < totalSheet; i++) { + result.add(this.workbook.getSheetAt(i)); + } + return result; + } + + /** + * 获取表名列表 + * + * @return 表名列表 + * @since 4.0.3 + */ + public List getSheetNames() { + final int totalSheet = workbook.getNumberOfSheets(); + List result = new ArrayList<>(totalSheet); + for (int i = 0; i < totalSheet; i++) { + result.add(this.workbook.getSheetAt(i).getSheetName()); + } + return result; + } + + /** + * 获取当前Sheet + * + * @return {@link Sheet} + */ + public Sheet getSheet() { + return this.sheet; + } + + /** + * 自定义需要读取或写出的Sheet,如果给定的sheet不存在,创建之。
+ * 在读取中,此方法用于切换读取的sheet,在写出时,此方法用于新建或者切换sheet。 + * + * @param sheetName sheet名 + * @return this + * @since 4.0.10 + */ + @SuppressWarnings("unchecked") + public T setSheet(String sheetName) { + this.sheet = this.workbook.getSheet(sheetName); + if (null == this.sheet) { + this.sheet = this.workbook.createSheet(sheetName); + } + return (T) this; + } + + /** + * 自定义需要读取或写出的Sheet,如果给定的sheet不存在,创建之(命名为默认)
+ * 在读取中,此方法用于切换读取的sheet,在写出时,此方法用于新建或者切换sheet + * + * @param sheetIndex sheet序号,从0开始计数 + * @return this + * @since 4.0.10 + */ + @SuppressWarnings("unchecked") + public T setSheet(int sheetIndex) { + try { + this.sheet = this.workbook.getSheetAt(sheetIndex); + } catch (IllegalArgumentException e) { + this.sheet = this.workbook.createSheet(); + } + if (null == this.sheet) { + this.sheet = this.workbook.createSheet(); + } + return (T) this; + } + + /** + * 获取指定坐标单元格,单元格不存在时返回null + * + * @param x X坐标,从0计数,既列号 + * @param y Y坐标,从0计数,既行号 + * @return {@link Cell} + * @since 4.0.5 + */ + public Cell getCell(int x, int y) { + return getCell(x, y, false); + } + + /** + * 获取或创建指定坐标单元格 + * + * @param x X坐标,从0计数,既列号 + * @param y Y坐标,从0计数,既行号 + * @return {@link Cell} + * @since 4.0.6 + */ + public Cell getOrCreateCell(int x, int y) { + return getCell(x, y, true); + } + + /** + * 获取指定坐标单元格,如果isCreateIfNotExist为false,则在单元格不存在时返回null + * + * @param x X坐标,从0计数,既列号 + * @param y Y坐标,从0计数,既行号 + * @param isCreateIfNotExist 单元格不存在时是否创建 + * @return {@link Cell} + * @since 4.0.6 + */ + public Cell getCell(int x, int y, boolean isCreateIfNotExist) { + final Row row = isCreateIfNotExist ? RowUtil.getOrCreateRow(this.sheet, y) : this.sheet.getRow(y); + if (null != row) { + return isCreateIfNotExist ? CellUtil.getOrCreateCell(row, x) : row.getCell(x); + } + return null; + } + + /** + * 获取或者创建行 + * + * @param y Y坐标,从0计数,既行号 + * @return {@link Row} + * @since 4.1.4 + */ + public Row getOrCreateRow(int y) { + return RowUtil.getOrCreateRow(this.sheet, y); + } + + /** + * 为指定单元格获取或者创建样式,返回样式后可以设置样式内容 + * + * @param x X坐标,从0计数,既列号 + * @param y Y坐标,从0计数,既行号 + * @return {@link CellStyle} + * @since 4.1.4 + */ + public CellStyle getOrCreateCellStyle(int x, int y) { + final Cell cell = getOrCreateCell(x, y); + CellStyle cellStyle = cell.getCellStyle(); + if (null == cellStyle) { + cellStyle = this.workbook.createCellStyle(); + cell.setCellStyle(cellStyle); + } + return cellStyle; + } + + /** + * 获取或创建某一行的样式,返回样式后可以设置样式内容 + * + * @param y Y坐标,从0计数,既行号 + * @return {@link CellStyle} + * @since 4.1.4 + */ + public CellStyle getOrCreateRowStyle(int y) { + final Row row = getOrCreateRow(y); + CellStyle rowStyle = row.getRowStyle(); + if (null == rowStyle) { + rowStyle = this.workbook.createCellStyle(); + row.setRowStyle(rowStyle); + } + return rowStyle; + } + + /** + * 获取或创建某一行的样式,返回样式后可以设置样式内容 + * + * @param x X坐标,从0计数,既列号 + * @return {@link CellStyle} + * @since 4.1.4 + */ + public CellStyle getOrCreateColumnStyle(int x) { + CellStyle columnStyle = this.sheet.getColumnStyle(x); + if (null == columnStyle) { + columnStyle = this.workbook.createCellStyle(); + this.sheet.setDefaultColumnStyle(x, columnStyle); + } + return columnStyle; + } + + /** + * 获取总行数,计算方法为: + * + *

+	 * 最后一行序号 + 1
+	 * 
+ * + * @return 行数 + * @since 4.5.4 + */ + public int getRowCount() { + return this.sheet.getLastRowNum() + 1; + } + + /** + * 获取有记录的行数,计算方法为: + * + *
+	 * 最后一行序号 - 第一行序号 + 1
+	 * 
+ * + * @return 行数 + * @since 4.5.4 + */ + public int getPhysicalRowCount() { + return this.sheet.getPhysicalNumberOfRows(); + } + + /** + * 获取第一行总列数,计算方法为: + * + *
+	 * 最后一列序号 + 1
+	 * 
+ * + * @return 列数 + */ + public int getColumnCount() { + return getColumnCount(0); + } + + /** + * 获取总列数,计算方法为: + * + *
+	 * 最后一列序号 + 1
+	 * 
+ * + * @param rowNum 行号 + * @return 列数 + */ + public int getColumnCount(int rowNum) { + // getLastCellNum方法返回序号+1的值 + return this.sheet.getRow(rowNum).getLastCellNum(); + } + + /** + * 判断是否为xlsx格式的Excel表(Excel07格式) + * + * @return 是否为xlsx格式的Excel表(Excel07格式) + * @since 4.6.2 + */ + public boolean isXlsx() { + return this.sheet instanceof XSSFSheet; + } + + /** + * 关闭工作簿
+ * 如果用户设定了目标文件,先写出目标文件后给关闭工作簿 + */ + @Override + public void close() { + IoUtil.close(this.workbook); + this.sheet = null; + this.workbook = null; + this.isClosed = true; + } +} diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelFileUtil.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelFileUtil.java new file mode 100644 index 000000000..b1b1f6e07 --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelFileUtil.java @@ -0,0 +1,54 @@ +package cn.hutool.poi.excel; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PushbackInputStream; + +import org.apache.poi.poifs.filesystem.FileMagic; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; + +/** + * Excel文件工具类 + * + * @author looly + * @since 4.2.1 + */ +public class ExcelFileUtil { + // ------------------------------------------------------------------------------------------------ isXls + /** + * 是否为XLS格式的Excel文件(HSSF)
+ * XLS文件主要用于Excel 97~2003创建 + * + * @param in excel输入流 + * @return 是否为XLS格式的Excel文件(HSSF) + */ + public static boolean isXls(InputStream in) { + final PushbackInputStream pin = IoUtil.toPushbackStream(in, 8); + try { + return FileMagic.valueOf(pin) == FileMagic.OLE2; + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 是否为XLSX格式的Excel文件(XSSF)
+ * XLSX文件主要用于Excel 2007+创建 + * + * @param in excel输入流 + * @return 是否为XLSX格式的Excel文件(XSSF) + */ + public static boolean isXlsx(InputStream in) { + if (false == in.markSupported()) { + in = new BufferedInputStream(in); + } + try { + return FileMagic.valueOf(in) == FileMagic.OOXML; + } catch (IOException e) { + throw new IORuntimeException(e); + } + } +} diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelPicUtil.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelPicUtil.java new file mode 100644 index 000000000..c0f7d5e2d --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelPicUtil.java @@ -0,0 +1,109 @@ +package cn.hutool.poi.excel; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.poi.POIXMLDocumentPart; +import org.apache.poi.hssf.usermodel.HSSFClientAnchor; +import org.apache.poi.hssf.usermodel.HSSFPicture; +import org.apache.poi.hssf.usermodel.HSSFPictureData; +import org.apache.poi.hssf.usermodel.HSSFShape; +import org.apache.poi.hssf.usermodel.HSSFSheet; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.usermodel.PictureData; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFDrawing; +import org.apache.poi.xssf.usermodel.XSSFPicture; +import org.apache.poi.xssf.usermodel.XSSFShape; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.openxmlformats.schemas.drawingml.x2006.spreadsheetDrawing.CTMarker; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; + +/** + * Excel图片工具类 + * + * @author looly + * @since 4.0.7 + */ +public class ExcelPicUtil { + /** + * 获取工作簿指定sheet中图片列表 + * + * @param workbook 工作簿{@link Workbook} + * @param sheetIndex sheet的索引 + * @return 图片映射,键格式:行_列,值:{@link PictureData} + */ + public static Map getPicMap(Workbook workbook, int sheetIndex) { + Assert.notNull(workbook, "Workbook must be not null !"); + if (sheetIndex < 0) { + sheetIndex = 0; + } + + if (workbook instanceof HSSFWorkbook) { + return getPicMapXls((HSSFWorkbook) workbook, sheetIndex); + } else if (workbook instanceof XSSFWorkbook) { + return getPicMapXlsx((XSSFWorkbook) workbook, sheetIndex); + } else { + throw new IllegalArgumentException(StrUtil.format("Workbook type [{}] is not supported!", workbook.getClass())); + } + } + + // -------------------------------------------------------------------------------------------------------------- Private method start + /** + * 获取XLS工作簿指定sheet中图片列表 + * + * @param workbook 工作簿{@link Workbook} + * @param sheetIndex sheet的索引 + * @return 图片映射,键格式:行_列,值:{@link PictureData} + */ + private static Map getPicMapXls(HSSFWorkbook workbook, int sheetIndex) { + final Map picMap = new HashMap<>(); + final List pictures = workbook.getAllPictures(); + if (CollectionUtil.isNotEmpty(pictures)) { + final HSSFSheet sheet = workbook.getSheetAt(sheetIndex); + HSSFClientAnchor anchor; + int pictureIndex; + for (HSSFShape shape : sheet.getDrawingPatriarch().getChildren()) { + if (shape instanceof HSSFPicture) { + pictureIndex = ((HSSFPicture) shape).getPictureIndex() - 1; + anchor = (HSSFClientAnchor) shape.getAnchor(); + picMap.put(StrUtil.format("{}_{}", anchor.getRow1(), anchor.getCol1()), pictures.get(pictureIndex)); + } + } + } + return picMap; + } + + /** + * 获取XLSX工作簿指定sheet中图片列表 + * + * @param workbook 工作簿{@link Workbook} + * @param sheetIndex sheet的索引 + * @return 图片映射,键格式:行_列,值:{@link PictureData} + */ + private static Map getPicMapXlsx(XSSFWorkbook workbook, int sheetIndex) { + final Map sheetIndexPicMap = new HashMap(); + final XSSFSheet sheet = workbook.getSheetAt(sheetIndex); + XSSFDrawing drawing; + for (POIXMLDocumentPart dr : sheet.getRelations()) { + if (dr instanceof XSSFDrawing) { + drawing = (XSSFDrawing) dr; + final List shapes = drawing.getShapes(); + XSSFPicture pic; + CTMarker ctMarker; + for (XSSFShape shape : shapes) { + pic = (XSSFPicture) shape; + ctMarker = pic.getPreferredSize().getFrom(); + sheetIndexPicMap.put(StrUtil.format("{}_{}", ctMarker.getRow(), ctMarker.getCol()), pic.getPictureData()); + } + } + } + return sheetIndexPicMap; + } + // -------------------------------------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelReader.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelReader.java new file mode 100644 index 000000000..db2719c98 --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelReader.java @@ -0,0 +1,482 @@ +package cn.hutool.poi.excel; + +import java.io.File; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.extractor.ExcelExtractor; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.extractor.XSSFExcelExtractor; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.IterUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.poi.excel.cell.CellEditor; +import cn.hutool.poi.excel.cell.CellUtil; +import cn.hutool.poi.excel.editors.TrimEditor; + +/** + * Excel读取器
+ * 读取Excel工作簿 + * + * @author Looly + * @since 3.1.0 + */ +public class ExcelReader extends ExcelBase { + + /** 是否忽略空行 */ + private boolean ignoreEmptyRow = true; + /** 单元格值处理接口 */ + private CellEditor cellEditor; + /** 标题别名 */ + private Map headerAlias = new HashMap<>(); + + // ------------------------------------------------------------------------------------------------------- Constructor start + /** + * 构造 + * + * @param excelFilePath Excel文件路径,绝对路径或相对于ClassPath路径 + * @param sheetIndex sheet序号,0表示第一个sheet + */ + public ExcelReader(String excelFilePath, int sheetIndex) { + this(FileUtil.file(excelFilePath), sheetIndex); + } + + /** + * 构造 + * + * @param bookFile Excel文件 + * @param sheetIndex sheet序号,0表示第一个sheet + */ + public ExcelReader(File bookFile, int sheetIndex) { + this(WorkbookUtil.createBook(bookFile), sheetIndex); + } + + /** + * 构造 + * + * @param bookFile Excel文件 + * @param sheetName sheet名,第一个默认是sheet1 + */ + public ExcelReader(File bookFile, String sheetName) { + this(WorkbookUtil.createBook(bookFile), sheetName); + } + + /** + * 构造 + * + * @param bookStream Excel文件的流 + * @param sheetIndex sheet序号,0表示第一个sheet + * @param closeAfterRead 读取结束是否关闭流 + */ + public ExcelReader(InputStream bookStream, int sheetIndex, boolean closeAfterRead) { + this(WorkbookUtil.createBook(bookStream, closeAfterRead), sheetIndex); + } + + /** + * 构造 + * + * @param bookStream Excel文件的流 + * @param sheetName sheet名,第一个默认是sheet1 + * @param closeAfterRead 读取结束是否关闭流 + */ + public ExcelReader(InputStream bookStream, String sheetName, boolean closeAfterRead) { + this(WorkbookUtil.createBook(bookStream, closeAfterRead), sheetName); + } + + /** + * 构造 + * + * @param book {@link Workbook} 表示一个Excel文件 + * @param sheetIndex sheet序号,0表示第一个sheet + */ + public ExcelReader(Workbook book, int sheetIndex) { + this(book.getSheetAt(sheetIndex)); + } + + /** + * 构造 + * + * @param book {@link Workbook} 表示一个Excel文件 + * @param sheetName sheet名,第一个默认是sheet1 + */ + public ExcelReader(Workbook book, String sheetName) { + this(book.getSheet(sheetName)); + } + + /** + * 构造 + * + * @param sheet Excel中的sheet + */ + public ExcelReader(Sheet sheet) { + super(sheet); + } + // ------------------------------------------------------------------------------------------------------- Constructor end + + // ------------------------------------------------------------------------------------------------------- Getters and Setters start + /** + * 是否忽略空行 + * + * @return 是否忽略空行 + */ + public boolean isIgnoreEmptyRow() { + return ignoreEmptyRow; + } + + /** + * 设置是否忽略空行 + * + * @param ignoreEmptyRow 是否忽略空行 + * @return this + */ + public ExcelReader setIgnoreEmptyRow(boolean ignoreEmptyRow) { + this.ignoreEmptyRow = ignoreEmptyRow; + return this; + } + + /** + * 设置单元格值处理逻辑
+ * 当Excel中的值并不能满足我们的读取要求时,通过传入一个编辑接口,可以对单元格值自定义,例如对数字和日期类型值转换为字符串等 + * + * @param cellEditor 单元格值处理接口 + * @return this + * @see TrimEditor + */ + public ExcelReader setCellEditor(CellEditor cellEditor) { + this.cellEditor = cellEditor; + return this; + } + + /** + * 获得标题行的别名Map + * + * @return 别名Map + */ + public Map getHeaderAlias() { + return headerAlias; + } + + /** + * 设置标题行的别名Map + * + * @param headerAlias 别名Map + * @return this + */ + public ExcelReader setHeaderAlias(Map headerAlias) { + this.headerAlias = headerAlias; + return this; + } + + /** + * 增加标题别名 + * + * @param header 标题 + * @param alias 别名 + * @return this + */ + public ExcelReader addHeaderAlias(String header, String alias) { + this.headerAlias.put(header, alias); + return this; + } + + /** + * 去除标题别名 + * + * @param header 标题 + * @return this + */ + public ExcelReader removeHeaderAlias(String header) { + this.headerAlias.remove(header); + return this; + } + // ------------------------------------------------------------------------------------------------------- Getters and Setters end + + /** + * 读取工作簿中指定的Sheet的所有行列数据 + * + * @return 行的集合,一行使用List表示 + */ + public List> read() { + return read(0); + } + + /** + * 读取工作簿中指定的Sheet + * + * @param startRowIndex 起始行(包含,从0开始计数) + * @return 行的集合,一行使用List表示 + * @since 4.0.0 + */ + public List> read(int startRowIndex) { + return read(startRowIndex, Integer.MAX_VALUE); + } + + /** + * 读取工作簿中指定的Sheet + * + * @param startRowIndex 起始行(包含,从0开始计数) + * @param endRowIndex 结束行(包含,从0开始计数) + * @return 行的集合,一行使用List表示 + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + public List> read(int startRowIndex, int endRowIndex) { + checkNotClosed(); + List> resultList = new ArrayList<>(); + + startRowIndex = Math.max(startRowIndex, this.sheet.getFirstRowNum());// 读取起始行(包含) + endRowIndex = Math.min(endRowIndex, this.sheet.getLastRowNum());// 读取结束行(包含) + boolean isFirstLine = true; + List rowList; + for (int i = startRowIndex; i <= endRowIndex; i++) { + rowList = readRow(i); + if (CollUtil.isNotEmpty(rowList) || false == ignoreEmptyRow) { + if (null == rowList) { + rowList = new ArrayList<>(0); + } + if (isFirstLine) { + isFirstLine = false; + if (MapUtil.isNotEmpty(this.headerAlias)) { + rowList = aliasHeader(rowList); + } + } + resultList.add(rowList); + } + } + return resultList; + } + + /** + * 读取Excel为Map的列表,读取所有行,默认第一行做为标题,数据从第二行开始
+ * Map表示一行,标题为key,单元格内容为value + * + * @return Map的列表 + */ + public List> readAll() { + return read(0, 1, Integer.MAX_VALUE); + } + + /** + * 读取Excel为Map的列表
+ * Map表示一行,标题为key,单元格内容为value + * + * @param headerRowIndex 标题所在行,如果标题行在读取的内容行中间,这行做为数据将忽略 + * @param startRowIndex 起始行(包含,从0开始计数) + * @param endRowIndex 读取结束行(包含,从0开始计数) + * @return Map的列表 + */ + public List> read(int headerRowIndex, int startRowIndex, int endRowIndex) { + checkNotClosed(); + // 边界判断 + final int firstRowNum = sheet.getFirstRowNum(); + final int lastRowNum = sheet.getLastRowNum(); + if (headerRowIndex < firstRowNum) { + throw new IndexOutOfBoundsException(StrUtil.format("Header row index {} is lower than first row index {}.", headerRowIndex, firstRowNum)); + } else if (headerRowIndex > lastRowNum) { + throw new IndexOutOfBoundsException(StrUtil.format("Header row index {} is greater than last row index {}.", headerRowIndex, firstRowNum)); + } + startRowIndex = Math.max(startRowIndex, firstRowNum);// 读取起始行(包含) + endRowIndex = Math.min(endRowIndex, lastRowNum);// 读取结束行(包含) + + // 读取header + List headerList = readRow(sheet.getRow(headerRowIndex)); + + final List> result = new ArrayList<>(endRowIndex - startRowIndex + 1); + List rowList; + for (int i = startRowIndex; i <= endRowIndex; i++) { + if (i != headerRowIndex) { + // 跳过标题行 + rowList = readRow(sheet.getRow(i)); + if (CollUtil.isNotEmpty(rowList) || false == ignoreEmptyRow) { + if (null == rowList) { + rowList = new ArrayList<>(0); + } + result.add(IterUtil.toMap(aliasHeader(headerList), rowList, true)); + } + } + } + return result; + } + + /** + * 读取Excel为Bean的列表,读取所有行,默认第一行做为标题,数据从第二行开始 + * + * @param Bean类型 + * @param beanType 每行对应Bean的类型 + * @return Map的列表 + */ + public List readAll(Class beanType) { + return read(0, 1, Integer.MAX_VALUE, beanType); + } + + /** + * 读取Excel为Bean的列表 + * + * @param Bean类型 + * @param headerRowIndex 标题所在行,如果标题行在读取的内容行中间,这行做为数据将忽略,,从0开始计数 + * @param startRowIndex 起始行(包含,从0开始计数) + * @param beanType 每行对应Bean的类型 + * @return Map的列表 + * @since 4.0.1 + */ + public List read(int headerRowIndex, int startRowIndex, Class beanType) { + return read(headerRowIndex, startRowIndex, Integer.MAX_VALUE, beanType); + } + + /** + * 读取Excel为Bean的列表 + * + * @param Bean类型 + * @param headerRowIndex 标题所在行,如果标题行在读取的内容行中间,这行做为数据将忽略,,从0开始计数 + * @param startRowIndex 起始行(包含,从0开始计数) + * @param endRowIndex 读取结束行(包含,从0开始计数) + * @param beanType 每行对应Bean的类型 + * @return Map的列表 + */ + @SuppressWarnings("unchecked") + public List read(int headerRowIndex, int startRowIndex, int endRowIndex, Class beanType) { + checkNotClosed(); + final List> mapList = read(headerRowIndex, startRowIndex, endRowIndex); + if (Map.class.isAssignableFrom(beanType)) { + return (List) mapList; + } + + final List beanList = new ArrayList<>(mapList.size()); + for (Map map : mapList) { + beanList.add(BeanUtil.mapToBean(map, beanType, false)); + } + return beanList; + } + + /** + * 读取为文本格式
+ * 使用{@link ExcelExtractor} 提取Excel内容 + * + * @param withSheetName 是否附带sheet名 + * @return Excel文本 + * @since 4.1.0 + */ + public String readAsText(boolean withSheetName) { + final ExcelExtractor extractor = getExtractor(); + extractor.setIncludeSheetNames(withSheetName); + return extractor.getText(); + } + + /** + * 获取 {@link ExcelExtractor} 对象 + * + * @return {@link ExcelExtractor} + * @since 4.1.0 + */ + public ExcelExtractor getExtractor() { + ExcelExtractor extractor; + Workbook wb = this.workbook; + if (wb instanceof HSSFWorkbook) { + extractor = new org.apache.poi.hssf.extractor.ExcelExtractor((HSSFWorkbook) wb); + } else { + extractor = new XSSFExcelExtractor((XSSFWorkbook) wb); + } + return extractor; + } + + /** + * 读取某一行数据 + * + * @param rowIndex 行号,从0开始 + * @return 一行数据 + * @since 4.0.3 + */ + public List readRow(int rowIndex) { + return readRow(this.sheet.getRow(rowIndex)); + } + + /** + * 读取某个单元格的值 + * + * @param x X坐标,从0计数,既列号 + * @param y Y坐标,从0计数,既行号 + * @return 值,如果单元格无值返回null + * @since 4.0.3 + */ + public Object readCellValue(int x, int y) { + return CellUtil.getCellValue(getCell(x, y), this.cellEditor); + } + + /** + * 获取Excel写出器
+ * 在读取Excel并做一定编辑后,获取写出器写出 + * + * @return {@link ExcelWriter} + * @since 4.0.6 + */ + public ExcelWriter getWriter() { + return new ExcelWriter(this.sheet); + } + + // ------------------------------------------------------------------------------------------------------- Private methods start + /** + * 读取一行 + * + * @param row 行 + * @return 单元格值列表 + */ + private List readRow(Row row) { + return RowUtil.readRow(row, this.cellEditor); + } + + /** + * 转换标题别名,如果没有别名则使用原标题,当标题为空时,列号对应的字母便是header + * + * @param headerList 原标题列表 + * @return 转换别名列表 + */ + private List aliasHeader(List headerList) { + final int size = headerList.size(); + final ArrayList result = new ArrayList<>(size); + if (CollUtil.isEmpty(headerList)) { + return result; + } + + for(int i = 0; i < size; i++) { + result.add(aliasHeader(headerList.get(i), i)); + } + return result; + } + + /** + * 转换标题别名,如果没有别名则使用原标题,当标题为空时,列号对应的字母便是header + * + * @param headerObj 原标题 + * @param index 标题所在列号,当标题为空时,列号对应的字母便是header + * @return 转换别名列表 + * @since 4.3.2 + */ + private String aliasHeader(Object headerObj, int index) { + if(null == headerObj) { + return ExcelUtil.indexToColName(index); + } + + final String header = headerObj.toString(); + return ObjectUtil.defaultIfNull(this.headerAlias.get(header), header); + } + + /** + * 检查是否未关闭状态 + */ + private void checkNotClosed() { + Assert.isFalse(this.isClosed, "ExcelReader has been closed!"); + } + // ------------------------------------------------------------------------------------------------------- Private methods end +} diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelUtil.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelUtil.java new file mode 100644 index 000000000..a21ef45cc --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelUtil.java @@ -0,0 +1,588 @@ +package cn.hutool.poi.excel; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; + +import cn.hutool.core.exceptions.DependencyException; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.poi.PoiChecker; +import cn.hutool.poi.excel.sax.Excel03SaxReader; +import cn.hutool.poi.excel.sax.Excel07SaxReader; +import cn.hutool.poi.excel.sax.handler.RowHandler; + +/** + * Excel工具类 + * + * @author Looly + * + */ +public class ExcelUtil { + + // ------------------------------------------------------------------------------------ Read by Sax start + /** + * 通过Sax方式读取Excel,同时支持03和07格式 + * + * @param path Excel文件路径 + * @param sheetIndex sheet序号 + * @param rowHandler 行处理器 + * @since 3.2.0 + */ + public static void readBySax(String path, int sheetIndex, RowHandler rowHandler) { + BufferedInputStream in = null; + try { + in = FileUtil.getInputStream(path); + readBySax(in, sheetIndex, rowHandler); + } finally { + IoUtil.close(in); + } + } + + /** + * 通过Sax方式读取Excel,同时支持03和07格式 + * + * @param file Excel文件 + * @param sheetIndex sheet序号 + * @param rowHandler 行处理器 + * @since 3.2.0 + */ + public static void readBySax(File file, int sheetIndex, RowHandler rowHandler) { + BufferedInputStream in = null; + try { + in = FileUtil.getInputStream(file); + readBySax(in, sheetIndex, rowHandler); + } finally { + IoUtil.close(in); + } + } + + /** + * 通过Sax方式读取Excel,同时支持03和07格式 + * + * @param in Excel流 + * @param sheetIndex sheet序号 + * @param rowHandler 行处理器 + * @since 3.2.0 + */ + public static void readBySax(InputStream in, int sheetIndex, RowHandler rowHandler) { + in = IoUtil.toMarkSupportStream(in); + if (ExcelFileUtil.isXlsx(in)) { + read07BySax(in, sheetIndex, rowHandler); + } else { + read03BySax(in, sheetIndex, rowHandler); + } + } + + /** + * Sax方式读取Excel07 + * + * @param in 输入流 + * @param sheetIndex Sheet索引,-1表示全部Sheet, 0表示第一个Sheet + * @param rowHandler 行处理器 + * @return {@link Excel07SaxReader} + * @since 3.2.0 + */ + public static Excel07SaxReader read07BySax(InputStream in, int sheetIndex, RowHandler rowHandler) { + try { + return new Excel07SaxReader(rowHandler).read(in, sheetIndex); + } catch (NoClassDefFoundError e) { + throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG); + } + } + + /** + * Sax方式读取Excel07 + * + * @param file 文件 + * @param sheetIndex Sheet索引,-1表示全部Sheet, 0表示第一个Sheet + * @param rowHandler 行处理器 + * @return {@link Excel07SaxReader} + * @since 3.2.0 + */ + public static Excel07SaxReader read07BySax(File file, int sheetIndex, RowHandler rowHandler) { + try { + return new Excel07SaxReader(rowHandler).read(file, sheetIndex); + } catch (NoClassDefFoundError e) { + throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG); + } + } + + /** + * Sax方式读取Excel07 + * + * @param path 路径 + * @param sheetIndex Sheet索引,-1表示全部Sheet, 0表示第一个Sheet + * @param rowHandler 行处理器 + * @return {@link Excel07SaxReader} + * @since 3.2.0 + */ + public static Excel07SaxReader read07BySax(String path, int sheetIndex, RowHandler rowHandler) { + try { + return new Excel07SaxReader(rowHandler).read(path, sheetIndex); + } catch (NoClassDefFoundError e) { + throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG); + } + } + + /** + * Sax方式读取Excel03 + * + * @param in 输入流 + * @param sheetIndex Sheet索引,-1表示全部Sheet, 0表示第一个Sheet + * @param rowHandler 行处理器 + * @return {@link Excel07SaxReader} + * @since 3.2.0 + */ + public static Excel03SaxReader read03BySax(InputStream in, int sheetIndex, RowHandler rowHandler) { + try { + return new Excel03SaxReader(rowHandler).read(in, sheetIndex); + } catch (NoClassDefFoundError e) { + throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG); + } + } + + /** + * Sax方式读取Excel03 + * + * @param file 文件 + * @param sheetIndex Sheet索引,-1表示全部Sheet, 0表示第一个Sheet + * @param rowHandler 行处理器 + * @return {@link Excel03SaxReader} + * @since 3.2.0 + */ + public static Excel03SaxReader read03BySax(File file, int sheetIndex, RowHandler rowHandler) { + try { + return new Excel03SaxReader(rowHandler).read(file, sheetIndex); + } catch (NoClassDefFoundError e) { + throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG); + } + } + + /** + * Sax方式读取Excel03 + * + * @param path 路径 + * @param sheetIndex Sheet索引,-1表示全部Sheet, 0表示第一个Sheet + * @param rowHandler 行处理器 + * @return {@link Excel03SaxReader} + * @since 3.2.0 + */ + public static Excel03SaxReader read03BySax(String path, int sheetIndex, RowHandler rowHandler) { + try { + return new Excel03SaxReader(rowHandler).read(path, sheetIndex); + } catch (NoClassDefFoundError e) { + throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG); + } + } + // ------------------------------------------------------------------------------------ Read by Sax end + + // ------------------------------------------------------------------------------------------------ getReader + /** + * 获取Excel读取器,通过调用{@link ExcelReader}的read或readXXX方法读取Excel内容
+ * 默认调用第一个sheet + * + * @param bookFilePath Excel文件路径,绝对路径或相对于ClassPath路径 + * @return {@link ExcelReader} + * @since 3.1.1 + */ + public static ExcelReader getReader(String bookFilePath) { + return getReader(bookFilePath, 0); + } + + /** + * 获取Excel读取器,通过调用{@link ExcelReader}的read或readXXX方法读取Excel内容
+ * 默认调用第一个sheet + * + * @param bookFile Excel文件 + * @return {@link ExcelReader} + */ + public static ExcelReader getReader(File bookFile) { + return getReader(bookFile, 0); + } + + /** + * 获取Excel读取器,通过调用{@link ExcelReader}的read或readXXX方法读取Excel内容 + * + * @param bookFilePath Excel文件路径,绝对路径或相对于ClassPath路径 + * @param sheetIndex sheet序号,0表示第一个sheet + * @return {@link ExcelReader} + * @since 3.1.1 + */ + public static ExcelReader getReader(String bookFilePath, int sheetIndex) { + try { + return new ExcelReader(bookFilePath, sheetIndex); + } catch (NoClassDefFoundError e) { + throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG); + } + } + + /** + * 获取Excel读取器,通过调用{@link ExcelReader}的read或readXXX方法读取Excel内容 + * + * @param bookFile Excel文件 + * @param sheetIndex sheet序号,0表示第一个sheet + * @return {@link ExcelReader} + */ + public static ExcelReader getReader(File bookFile, int sheetIndex) { + try { + return new ExcelReader(bookFile, sheetIndex); + } catch (NoClassDefFoundError e) { + throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG); + } + } + + /** + * 获取Excel读取器,通过调用{@link ExcelReader}的read或readXXX方法读取Excel内容 + * + * @param bookFile Excel文件 + * @param sheetName sheet名,第一个默认是sheet1 + * @return {@link ExcelReader} + */ + public static ExcelReader getReader(File bookFile, String sheetName) { + try { + return new ExcelReader(bookFile, sheetName); + } catch (NoClassDefFoundError e) { + throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG); + } + } + + /** + * 获取Excel读取器,通过调用{@link ExcelReader}的read或readXXX方法读取Excel内容
+ * 默认调用第一个sheet,读取结束自动关闭流 + * + * @param bookStream Excel文件的流 + * @return {@link ExcelReader} + */ + public static ExcelReader getReader(InputStream bookStream) { + return getReader(bookStream, 0, true); + } + + /** + * 获取Excel读取器,通过调用{@link ExcelReader}的read或readXXX方法读取Excel内容
+ * 默认调用第一个sheet + * + * @param bookStream Excel文件的流 + * @param closeAfterRead 读取结束是否关闭流 + * @return {@link ExcelReader} + * @since 4.0.3 + */ + public static ExcelReader getReader(InputStream bookStream, boolean closeAfterRead) { + try { + return getReader(bookStream, 0, closeAfterRead); + } catch (NoClassDefFoundError e) { + throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG); + } + } + + /** + * 获取Excel读取器,通过调用{@link ExcelReader}的read或readXXX方法读取Excel内容
+ * 读取结束自动关闭流 + * + * @param bookStream Excel文件的流 + * @param sheetIndex sheet序号,0表示第一个sheet + * @return {@link ExcelReader} + */ + public static ExcelReader getReader(InputStream bookStream, int sheetIndex) { + try { + return new ExcelReader(bookStream, sheetIndex, true); + } catch (NoClassDefFoundError e) { + throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG); + } + } + + /** + * 获取Excel读取器,通过调用{@link ExcelReader}的read或readXXX方法读取Excel内容 + * + * @param bookStream Excel文件的流 + * @param sheetIndex sheet序号,0表示第一个sheet + * @param closeAfterRead 读取结束是否关闭流 + * @return {@link ExcelReader} + * @since 4.0.3 + */ + public static ExcelReader getReader(InputStream bookStream, int sheetIndex, boolean closeAfterRead) { + try { + return new ExcelReader(bookStream, sheetIndex, closeAfterRead); + } catch (NoClassDefFoundError e) { + throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG); + } + } + + /** + * 获取Excel读取器,通过调用{@link ExcelReader}的read或readXXX方法读取Excel内容
+ * 读取结束自动关闭流 + * + * @param bookStream Excel文件的流 + * @param sheetName sheet名,第一个默认是sheet1 + * @return {@link ExcelReader} + */ + public static ExcelReader getReader(InputStream bookStream, String sheetName) { + try { + return new ExcelReader(bookStream, sheetName, true); + } catch (NoClassDefFoundError e) { + throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG); + } + } + + /** + * 获取Excel读取器,通过调用{@link ExcelReader}的read或readXXX方法读取Excel内容 + * + * @param bookStream Excel文件的流 + * @param sheetName sheet名,第一个默认是sheet1 + * @param closeAfterRead 读取结束是否关闭流 + * @return {@link ExcelReader} + */ + public static ExcelReader getReader(InputStream bookStream, String sheetName, boolean closeAfterRead) { + try { + return new ExcelReader(bookStream, sheetName, closeAfterRead); + } catch (NoClassDefFoundError e) { + throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG); + } + } + + // ------------------------------------------------------------------------------------------------ getWriter + /** + * 获得{@link ExcelWriter},默认写出到第一个sheet
+ * 不传入写出的Excel文件路径,只能调用{@link ExcelWriter#flush(OutputStream)}方法写出到流
+ * 若写出到文件,还需调用{@link ExcelWriter#setDestFile(File)}方法自定义写出的文件,然后调用{@link ExcelWriter#flush()}方法写出到文件 + * + * @return {@link ExcelWriter} + * @since 3.2.1 + */ + public static ExcelWriter getWriter() { + try { + return new ExcelWriter(); + } catch (NoClassDefFoundError e) { + throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG); + } + } + + /** + * 获得{@link ExcelWriter},默认写出到第一个sheet
+ * 不传入写出的Excel文件路径,只能调用{@link ExcelWriter#flush(OutputStream)}方法写出到流
+ * 若写出到文件,还需调用{@link ExcelWriter#setDestFile(File)}方法自定义写出的文件,然后调用{@link ExcelWriter#flush()}方法写出到文件 + * + * @param isXlsx 是否为xlsx格式 + * @return {@link ExcelWriter} + * @since 3.2.1 + */ + public static ExcelWriter getWriter(boolean isXlsx) { + try { + return new ExcelWriter(isXlsx); + } catch (NoClassDefFoundError e) { + throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG); + } + } + + /** + * 获得{@link ExcelWriter},默认写出到第一个sheet + * + * @param destFilePath 目标文件路径 + * @return {@link ExcelWriter} + */ + public static ExcelWriter getWriter(String destFilePath) { + try { + return new ExcelWriter(destFilePath); + } catch (NoClassDefFoundError e) { + throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG); + } + } + + /** + * 获得{@link ExcelWriter},默认写出到第一个sheet + * + * @param sheetName Sheet名 + * @return {@link ExcelWriter} + * @since 4.5.18 + */ + public static ExcelWriter getWriterWithSheet(String sheetName) { + try { + return new ExcelWriter((File)null, sheetName); + } catch (NoClassDefFoundError e) { + throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG); + } + } + + /** + * 获得{@link ExcelWriter},默认写出到第一个sheet,名字为sheet1 + * + * @param destFile 目标文件 + * @return {@link ExcelWriter} + */ + public static ExcelWriter getWriter(File destFile) { + try { + return new ExcelWriter(destFile); + } catch (NoClassDefFoundError e) { + throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG); + } + } + + /** + * 获得{@link ExcelWriter} + * + * @param destFilePath 目标文件路径 + * @param sheetName sheet表名 + * @return {@link ExcelWriter} + */ + public static ExcelWriter getWriter(String destFilePath, String sheetName) { + try { + return new ExcelWriter(destFilePath, sheetName); + } catch (NoClassDefFoundError e) { + throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG); + } + } + + /** + * 获得{@link ExcelWriter} + * + * @param destFile 目标文件 + * @param sheetName sheet表名 + * @return {@link ExcelWriter} + */ + public static ExcelWriter getWriter(File destFile, String sheetName) { + try { + return new ExcelWriter(destFile, sheetName); + } catch (NoClassDefFoundError e) { + throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG); + } + } + + // ------------------------------------------------------------------------------------------------ getBigWriter + /** + * 获得{@link BigExcelWriter},默认写出到第一个sheet
+ * 不传入写出的Excel文件路径,只能调用{@link BigExcelWriter#flush(OutputStream)}方法写出到流
+ * 若写出到文件,还需调用{@link BigExcelWriter#setDestFile(File)}方法自定义写出的文件,然后调用{@link BigExcelWriter#flush()}方法写出到文件 + * + * @return {@link BigExcelWriter} + * @since 4.1.13 + */ + public static ExcelWriter getBigWriter() { + try { + return new BigExcelWriter(); + } catch (NoClassDefFoundError e) { + throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG); + } + } + + /** + * 获得{@link BigExcelWriter},默认写出到第一个sheet
+ * 不传入写出的Excel文件路径,只能调用{@link BigExcelWriter#flush(OutputStream)}方法写出到流
+ * 若写出到文件,还需调用{@link BigExcelWriter#setDestFile(File)}方法自定义写出的文件,然后调用{@link BigExcelWriter#flush()}方法写出到文件 + * + * @param rowAccessWindowSize 在内存中的行数 + * @return {@link BigExcelWriter} + * @since 4.1.13 + */ + public static ExcelWriter getBigWriter(int rowAccessWindowSize) { + try { + return new BigExcelWriter(rowAccessWindowSize); + } catch (NoClassDefFoundError e) { + throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG); + } + } + + /** + * 获得{@link BigExcelWriter},默认写出到第一个sheet + * + * @param destFilePath 目标文件路径 + * @return {@link BigExcelWriter} + */ + public static BigExcelWriter getBigWriter(String destFilePath) { + try { + return new BigExcelWriter(destFilePath); + } catch (NoClassDefFoundError e) { + throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG); + } + } + + /** + * 获得{@link BigExcelWriter},默认写出到第一个sheet,名字为sheet1 + * + * @param destFile 目标文件 + * @return {@link BigExcelWriter} + */ + public static BigExcelWriter getBigWriter(File destFile) { + try { + return new BigExcelWriter(destFile); + } catch (NoClassDefFoundError e) { + throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG); + } + } + + /** + * 获得{@link BigExcelWriter} + * + * @param destFilePath 目标文件路径 + * @param sheetName sheet表名 + * @return {@link BigExcelWriter} + */ + public static BigExcelWriter getBigWriter(String destFilePath, String sheetName) { + try { + return new BigExcelWriter(destFilePath, sheetName); + } catch (NoClassDefFoundError e) { + throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG); + } + } + + /** + * 获得{@link BigExcelWriter} + * + * @param destFile 目标文件 + * @param sheetName sheet表名 + * @return {@link BigExcelWriter} + */ + public static BigExcelWriter getBigWriter(File destFile, String sheetName) { + try { + return new BigExcelWriter(destFile, sheetName); + } catch (NoClassDefFoundError e) { + throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG); + } + } + + /** + * 将Sheet列号变为列名 + * + * @param index 列号, 从0开始 + * @return 0->A; 1->B...26->AA + * @since 4.1.20 + */ + public static String indexToColName(int index) { + if (index < 0) { + return null; + } + final StringBuilder colName = StrUtil.builder(); + do { + if (colName.length() > 0) { + index--; + } + int remainder = index % 26; + colName.append((char) (remainder + 'A')); + index = (int) ((index - remainder) / 26); + } while (index > 0); + return colName.reverse().toString(); + } + + /** + * 根据表元的列名转换为列号 + * + * @param colName 列名, 从A开始 + * @return A1->0; B1->1...AA1->26 + * @since 4.1.20 + */ + public static int colNameToIndex(String colName) { + int length = colName.length(); + char c; + int index = -1; + for (int i = 0; i < length; i++) { + c = Character.toUpperCase(colName.charAt(i)); + if (Character.isDigit(c)) { + break;// 确定指定的char值是否为数字 + } + index = (index + 1) * 26 + (int) c - 'A'; + } + return index; + } +} diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelWriter.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelWriter.java new file mode 100644 index 000000000..0fef5b45d --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelWriter.java @@ -0,0 +1,977 @@ +package cn.hutool.poi.excel; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.poi.hssf.usermodel.DVConstraint; +import org.apache.poi.hssf.usermodel.HSSFDataValidation; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.DataValidation; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.HeaderFooter; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.util.CellRangeAddressList; +import org.apache.poi.xssf.usermodel.XSSFDataValidationConstraint; +import org.apache.poi.xssf.usermodel.XSSFDataValidationHelper; +import org.apache.poi.xssf.usermodel.XSSFSheet; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.comparator.IndexedComparator; +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.poi.excel.cell.CellUtil; +import cn.hutool.poi.excel.style.Align; + +/** + * Excel 写入器
+ * 此工具用于通过POI将数据写出到Excel,此对象可完成以下两个功能 + * + *
+ * 1. 编辑已存在的Excel,可写出原Excel文件,也可写出到其它地方(到文件或到流)
+ * 2. 新建一个空的Excel工作簿,完成数据填充后写出(到文件或到流)
+ * 
+ * + * @author Looly + * @since 3.2.0 + */ +public class ExcelWriter extends ExcelBase { + + /** 目标文件 */ + protected File destFile; + /** 当前行 */ + private AtomicInteger currentRow = new AtomicInteger(0); + /** 标题行别名 */ + private Map headerAlias; + /** 是否只保留别名对应的字段 */ + private boolean onlyAlias; + /** 标题顺序比较器 */ + private Comparator aliasComparator; + /** 样式集,定义不同类型数据样式 */ + private StyleSet styleSet; + + // -------------------------------------------------------------------------- Constructor start + /** + * 构造,默认生成xls格式的Excel文件
+ * 此构造不传入写出的Excel文件路径,只能调用{@link #flush(OutputStream)}方法写出到流
+ * 若写出到文件,还需调用{@link #setDestFile(File)}方法自定义写出的文件,然后调用{@link #flush()}方法写出到文件 + * + * @since 3.2.1 + */ + public ExcelWriter() { + this(false); + } + + /** + * 构造
+ * 此构造不传入写出的Excel文件路径,只能调用{@link #flush(OutputStream)}方法写出到流
+ * 若写出到文件,需要调用{@link #flush(File)} 写出到文件 + * + * @param isXlsx 是否为xlsx格式 + * @since 3.2.1 + */ + public ExcelWriter(boolean isXlsx) { + this(WorkbookUtil.createBook(isXlsx), null); + } + + /** + * 构造,默认写出到第一个sheet,第一个sheet名为sheet1 + * + * @param destFilePath 目标文件路径,可以不存在 + */ + public ExcelWriter(String destFilePath) { + this(destFilePath, null); + } + + /** + * 构造
+ * 此构造不传入写出的Excel文件路径,只能调用{@link #flush(OutputStream)}方法写出到流
+ * 若写出到文件,需要调用{@link #flush(File)} 写出到文件 + * + * @param isXlsx 是否为xlsx格式 + * @param sheetName sheet名,第一个sheet名并写出到此sheet,例如sheet1 + * @since 4.1.8 + */ + public ExcelWriter(boolean isXlsx, String sheetName) { + this(WorkbookUtil.createBook(isXlsx), sheetName); + } + + /** + * 构造 + * + * @param destFilePath 目标文件路径,可以不存在 + * @param sheetName sheet名,第一个sheet名并写出到此sheet,例如sheet1 + */ + public ExcelWriter(String destFilePath, String sheetName) { + this(FileUtil.file(destFilePath), sheetName); + } + + /** + * 构造,默认写出到第一个sheet,第一个sheet名为sheet1 + * + * @param destFile 目标文件,可以不存在 + */ + public ExcelWriter(File destFile) { + this(destFile, null); + } + + /** + * 构造 + * + * @param destFile 目标文件,可以不存在 + * @param sheetName sheet名,做为第一个sheet名并写出到此sheet,例如sheet1 + */ + public ExcelWriter(File destFile, String sheetName) { + this(WorkbookUtil.createBookForWriter(destFile), sheetName); + this.destFile = destFile; + } + + /** + * 构造
+ * 此构造不传入写出的Excel文件路径,只能调用{@link #flush(OutputStream)}方法写出到流
+ * 若写出到文件,还需调用{@link #setDestFile(File)}方法自定义写出的文件,然后调用{@link #flush()}方法写出到文件 + * + * @param workbook {@link Workbook} + * @param sheetName sheet名,做为第一个sheet名并写出到此sheet,例如sheet1 + */ + public ExcelWriter(Workbook workbook, String sheetName) { + this(WorkbookUtil.getOrCreateSheet(workbook, sheetName)); + } + + /** + * 构造
+ * 此构造不传入写出的Excel文件路径,只能调用{@link #flush(OutputStream)}方法写出到流
+ * 若写出到文件,还需调用{@link #setDestFile(File)}方法自定义写出的文件,然后调用{@link #flush()}方法写出到文件 + * + * @param sheet {@link Sheet} + * @since 4.0.6 + */ + public ExcelWriter(Sheet sheet) { + super(sheet); + this.styleSet = new StyleSet(workbook); + } + + // -------------------------------------------------------------------------- Constructor end + + @Override + public ExcelWriter setSheet(int sheetIndex) { + // 切换到新sheet需要重置开始行 + reset(); + return super.setSheet(sheetIndex); + } + + @Override + public ExcelWriter setSheet(String sheetName) { + // 切换到新sheet需要重置开始行 + reset(); + return super.setSheet(sheetName); + } + + /** + * 重置Writer,包括: + * + *
+	 * 1. 当前行游标归零
+	 * 2. 清空别名比较器
+	 * 
+ * + * @return this + */ + public ExcelWriter reset() { + resetRow(); + this.aliasComparator = null; + return this; + } + + /** + * 重命名当前sheet + * + * @param sheetName 新的sheet名 + * @return this + * @since 4.1.8 + */ + public ExcelWriter renameSheet(String sheetName) { + return renameSheet(this.workbook.getSheetIndex(this.sheet), sheetName); + } + + /** + * 重命名sheet + * + * @param sheet sheet需要,0表示第一个sheet + * @param sheetName 新的sheet名 + * @return this + * @since 4.1.8 + */ + public ExcelWriter renameSheet(int sheet, String sheetName) { + this.workbook.setSheetName(sheet, sheetName); + return this; + } + + /** + * 设置所有列为自动宽度,不考虑合并单元格
+ * 此方法必须在指定列数据完全写出后调用才有效。
+ * 列数计算是通过第一行计算的 + * + * @return this + * @since 4.0.12 + */ + public ExcelWriter autoSizeColumnAll() { + final int columnCount = this.getColumnCount(); + for (int i = 0; i < columnCount; i++) { + autoSizeColumn(i); + } + return this; + } + + /** + * 设置某列为自动宽度,不考虑合并单元格
+ * 此方法必须在指定列数据完全写出后调用才有效。 + * + * @param columnIndex 第几列,从0计数 + * @return this + * @since 4.0.12 + */ + public ExcelWriter autoSizeColumn(int columnIndex) { + this.sheet.autoSizeColumn(columnIndex); + return this; + } + + /** + * 设置某列为自动宽度
+ * 此方法必须在指定列数据完全写出后调用才有效。 + * + * @param columnIndex 第几列,从0计数 + * @param useMergedCells 是否适用于合并单元格 + * @return this + * @since 3.3.0 + */ + public ExcelWriter autoSizeColumn(int columnIndex, boolean useMergedCells) { + this.sheet.autoSizeColumn(columnIndex, useMergedCells); + return this; + } + + /** + * 设置样式集,如果不使用样式,传入{@code null} + * + * @param styleSet 样式集,{@code null}表示无样式 + * @return this + * @since 4.1.11 + */ + public ExcelWriter setStyleSet(StyleSet styleSet) { + this.styleSet = styleSet; + return this; + } + + /** + * 获取样式集,样式集可以自定义包括:
+ * + *
+	 * 1. 头部样式
+	 * 2. 一般单元格样式
+	 * 3. 默认数字样式
+	 * 4. 默认日期样式
+	 * 
+ * + * @return 样式集 + * @since 4.0.0 + */ + public StyleSet getStyleSet() { + return this.styleSet; + } + + /** + * 获取头部样式,获取样式后可自定义样式 + * + * @return 头部样式 + */ + public CellStyle getHeadCellStyle() { + return this.styleSet.headCellStyle; + } + + /** + * 获取单元格样式,获取样式后可自定义样式 + * + * @return 单元格样式 + */ + public CellStyle getCellStyle() { + return this.styleSet.cellStyle; + } + + /** + * 获得当前行 + * + * @return 当前行 + */ + public int getCurrentRow() { + return this.currentRow.get(); + } + + /** + * 设置当前所在行 + * + * @param rowIndex 行号 + * @return this + */ + public ExcelWriter setCurrentRow(int rowIndex) { + this.currentRow.set(rowIndex); + return this; + } + + /** + * 跳过当前行 + * + * @return this + */ + public ExcelWriter passCurrentRow() { + this.currentRow.incrementAndGet(); + return this; + } + + /** + * 跳过指定行数 + * + * @param rows 跳过的行数 + * @return this + */ + public ExcelWriter passRows(int rows) { + this.currentRow.addAndGet(rows); + return this; + } + + /** + * 重置当前行为0 + * + * @return this + */ + public ExcelWriter resetRow() { + this.currentRow.set(0); + return this; + } + + /** + * 设置写出的目标文件 + * + * @param destFile 目标文件 + * @return this + */ + public ExcelWriter setDestFile(File destFile) { + this.destFile = destFile; + return this; + } + + /** + * 设置标题别名,key为Map中的key,value为别名 + * + * @param headerAlias 标题别名 + * @return this + * @since 3.2.1 + */ + public ExcelWriter setHeaderAlias(Map headerAlias) { + this.headerAlias = headerAlias; + return this; + } + + /** + * 清空标题别名,key为Map中的key,value为别名 + * + * @return this + * @since 4.5.4 + */ + public ExcelWriter clearHeaderAlias() { + this.headerAlias = null; + return this; + } + + /** + * 设置是否只保留别名中的字段值,如果为true,则不设置alias的字段将不被输出,false表示原样输出 + * + * @param isOnlyAlias 是否只保留别名中的字段值 + * @return this + * @since 4.1.22 + */ + public ExcelWriter setOnlyAlias(boolean isOnlyAlias) { + this.onlyAlias = isOnlyAlias; + return this; + } + + /** + * 增加标题别名 + * + * @param name 原标题 + * @param alias 别名 + * @return this + * @since 4.1.5 + */ + public ExcelWriter addHeaderAlias(String name, String alias) { + Map headerAlias = this.headerAlias; + if (null == headerAlias) { + headerAlias = new LinkedHashMap<>(); + } + this.headerAlias = headerAlias; + headerAlias.put(name, alias); + return this; + } + + /** + * 设置列宽(单位为一个字符的宽度,例如传入width为10,表示10个字符的宽度) + * + * @param columnIndex 列号(从0开始计数,-1表示所有列的默认宽度) + * @param width 宽度(单位1~256个字符宽度) + * @return this + * @since 4.0.8 + */ + public ExcelWriter setColumnWidth(int columnIndex, int width) { + if (columnIndex < 0) { + this.sheet.setDefaultColumnWidth(width); + } else { + this.sheet.setColumnWidth(columnIndex, width * 256); + } + return this; + } + + /** + * 设置行高,值为一个点的高度 + * + * @param rownum 行号(从0开始计数,-1表示所有行的默认高度) + * @param height 高度 + * @return this + * @since 4.0.8 + */ + public ExcelWriter setRowHeight(int rownum, int height) { + if (rownum < 0) { + this.sheet.setDefaultRowHeightInPoints(height); + } else { + final Row row = this.sheet.getRow(rownum); + if (null != row) { + row.setHeightInPoints(height); + } + } + return this; + } + + /** + * 设置Excel页眉或页脚 + * + * @param text 页脚的文本 + * @param align 对齐方式枚举 {@link Align} + * @param isFooter 是否为页脚,false表示页眉,true表示页脚 + * @return this + * @since 4.1.0 + */ + public ExcelWriter setHeaderOrFooter(String text, Align align, boolean isFooter) { + final HeaderFooter headerFooter = isFooter ? this.sheet.getFooter() : this.sheet.getHeader(); + switch (align) { + case LEFT: + headerFooter.setLeft(text); + break; + case RIGHT: + headerFooter.setRight(text); + break; + case CENTER: + headerFooter.setCenter(text); + break; + default: + break; + } + return this; + } + + /** + * 增加下拉列表 + * + * @param regions {@link CellRangeAddressList} 指定下拉列表所占的单元格范围 + * @param x x坐标,列号,从0开始 + * @param y y坐标,行号,从0开始 + * @return this + * @since 4.6.2 + */ + public ExcelWriter addSelect(int x, int y, String... selectList) { + return addSelect(new CellRangeAddressList(y, y, x, x), selectList); + } + + /** + * 增加下拉列表 + * + * @param regions {@link CellRangeAddressList} 指定下拉列表所占的单元格范围 + * @param + * @return this + * @since 4.6.2 + */ + public ExcelWriter addSelect(CellRangeAddressList regions, String... selectList) { + final DVConstraint constraint = DVConstraint.createExplicitListConstraint(selectList); + + // 绑定 + DataValidation dataValidation = null; + + if(this.sheet instanceof XSSFSheet) { + final XSSFDataValidationHelper dvHelper = new XSSFDataValidationHelper((XSSFSheet)sheet); + final XSSFDataValidationConstraint dvConstraint = (XSSFDataValidationConstraint) dvHelper.createExplicitListConstraint(selectList); + dataValidation = dvHelper.createValidation(dvConstraint, regions); + } else { + dataValidation = new HSSFDataValidation(regions, constraint); + } + + dataValidation.setSuppressDropDownArrow(true); + dataValidation.setShowErrorBox(true); + + return addValidationData(dataValidation); + } + + /** + * 增加单元格控制,比如下拉列表、日期验证、数字范围验证等 + * + * @param dataValidation {@link DataValidation} + * @return this + * @since 4.6.2 + */ + public ExcelWriter addValidationData(DataValidation dataValidation) { + this.sheet.addValidationData(dataValidation); + return this; + } + + /** + * 合并当前行的单元格
+ * 样式为默认标题样式,可使用{@link #getHeadCellStyle()}方法调用后自定义默认样式 + * + * @param lastColumn 合并到的最后一个列号 + * @return this + */ + public ExcelWriter merge(int lastColumn) { + return merge(lastColumn, null); + } + + /** + * 合并当前行的单元格,并写入对象到单元格
+ * 如果写到单元格中的内容非null,行号自动+1,否则当前行号不变
+ * 样式为默认标题样式,可使用{@link #getHeadCellStyle()}方法调用后自定义默认样式 + * + * @param lastColumn 合并到的最后一个列号 + * @param content 合并单元格后的内容 + * @return this + */ + public ExcelWriter merge(int lastColumn, Object content) { + return merge(lastColumn, content, true); + } + + /** + * 合并某行的单元格,并写入对象到单元格
+ * 如果写到单元格中的内容非null,行号自动+1,否则当前行号不变
+ * 样式为默认标题样式,可使用{@link #getHeadCellStyle()}方法调用后自定义默认样式 + * + * @param lastColumn 合并到的最后一个列号 + * @param content 合并单元格后的内容 + * @param isSetHeaderStyle 是否为合并后的单元格设置默认标题样式 + * @return this + * @since 4.0.10 + */ + public ExcelWriter merge(int lastColumn, Object content, boolean isSetHeaderStyle) { + Assert.isFalse(this.isClosed, "ExcelWriter has been closed!"); + + final int rowIndex = this.currentRow.get(); + merge(rowIndex, rowIndex, 0, lastColumn, content, isSetHeaderStyle); + + // 设置内容后跳到下一行 + if (null != content) { + this.currentRow.incrementAndGet(); + } + return this; + } + + /** + * 合并某行的单元格,并写入对象到单元格
+ * 如果写到单元格中的内容非null,行号自动+1,否则当前行号不变
+ * 样式为默认标题样式,可使用{@link #getHeadCellStyle()}方法调用后自定义默认样式 + * + * @param lastColumn 合并到的最后一个列号 + * @param content 合并单元格后的内容 + * @param isSetHeaderStyle 是否为合并后的单元格设置默认标题样式 + * @return this + * @since 4.0.10 + */ + public ExcelWriter merge(int firstRow, int lastRow, int firstColumn, int lastColumn, Object content, boolean isSetHeaderStyle) { + Assert.isFalse(this.isClosed, "ExcelWriter has been closed!"); + + final CellStyle style = (isSetHeaderStyle && null != this.styleSet && null != this.styleSet.headCellStyle) ? this.styleSet.headCellStyle : this.styleSet.cellStyle; + CellUtil.mergingCells(this.sheet, firstRow, lastRow, firstColumn, lastColumn, style); + + // 设置内容 + if (null != content) { + final Cell cell = getOrCreateCell(firstColumn, firstRow); + CellUtil.setCellValue(cell, content, this.styleSet, isSetHeaderStyle); + } + return this; + } + + /** + * 写出数据,本方法只是将数据写入Workbook中的Sheet,并不写出到文件
+ * 写出的起始行为当前行号,可使用{@link #getCurrentRow()}方法调用,根据写出的的行数,当前行号自动增加
+ * 样式为默认样式,可使用{@link #getCellStyle()}方法调用后自定义默认样式
+ * 默认的,当当前行号为0时,写出标题(如果为Map或Bean),否则不写标题 + * + *

+ * data中元素支持的类型有: + * + *

+	 * 1. Iterable,既元素为一个集合,元素被当作一行,data表示多行
+ * 2. Map,既元素为一个Map,第一个Map的keys作为首行,剩下的行为Map的values,data表示多行
+ * 3. Bean,既元素为一个Bean,第一个Bean的字段名列表会作为首行,剩下的行为Bean的字段值列表,data表示多行
+ * 4. 其它类型,按照基本类型输出(例如字符串) + *
+ * + * @param data 数据 + * @return this + */ + public ExcelWriter write(Iterable data) { + return write(data, 0 == getCurrentRow()); + } + + /** + * 写出数据,本方法只是将数据写入Workbook中的Sheet,并不写出到文件
+ * 写出的起始行为当前行号,可使用{@link #getCurrentRow()}方法调用,根据写出的的行数,当前行号自动增加
+ * 样式为默认样式,可使用{@link #getCellStyle()}方法调用后自定义默认样式 + * + *

+ * data中元素支持的类型有: + * + *

+	 * 1. Iterable,既元素为一个集合,元素被当作一行,data表示多行
+ * 2. Map,既元素为一个Map,第一个Map的keys作为首行,剩下的行为Map的values,data表示多行
+ * 3. Bean,既元素为一个Bean,第一个Bean的字段名列表会作为首行,剩下的行为Bean的字段值列表,data表示多行
+ * 4. 其它类型,按照基本类型输出(例如字符串) + *
+ * + * @param data 数据 + * @param isWriteKeyAsHead 是否强制写出标题行(Map或Bean) + * @return this + */ + public ExcelWriter write(Iterable data, boolean isWriteKeyAsHead) { + Assert.isFalse(this.isClosed, "ExcelWriter has been closed!"); + boolean isFirst = true; + for (Object object : data) { + writeRow(object, isFirst && isWriteKeyAsHead); + if (isFirst) { + isFirst = false; + } + } + return this; + } + + /** + * 写出数据,本方法只是将数据写入Workbook中的Sheet,并不写出到文件
+ * 写出的起始行为当前行号,可使用{@link #getCurrentRow()}方法调用,根据写出的的行数,当前行号自动增加
+ * 样式为默认样式,可使用{@link #getCellStyle()}方法调用后自定义默认样式
+ * data中元素支持的类型有: + * + *

+ * 1. Map,既元素为一个Map,第一个Map的keys作为首行,剩下的行为Map的values,data表示多行
+ * 2. Bean,既元素为一个Bean,第一个Bean的字段名列表会作为首行,剩下的行为Bean的字段值列表,data表示多行
+ *

+ * + * @param data 数据 + * @param comparator 比较器,用于字段名的排序 + * @return this + * @since 3.2.3 + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + public ExcelWriter write(Iterable data, Comparator comparator) { + Assert.isFalse(this.isClosed, "ExcelWriter has been closed!"); + boolean isFirstRow = true; + Map map; + for (Object obj : data) { + if (obj instanceof Map) { + map = new TreeMap<>(comparator); + map.putAll((Map) obj); + } else { + map = BeanUtil.beanToMap(obj, new TreeMap(comparator), false, false); + } + writeRow(map, isFirstRow); + if (isFirstRow) { + isFirstRow = false; + } + } + return this; + } + + /** + * 写出一行标题数据
+ * 本方法只是将数据写入Workbook中的Sheet,并不写出到文件
+ * 写出的起始行为当前行号,可使用{@link #getCurrentRow()}方法调用,根据写出的的行数,当前行号自动+1
+ * 样式为默认标题样式,可使用{@link #getHeadCellStyle()}方法调用后自定义默认样式 + * + * @param rowData 一行的数据 + * @return this + */ + public ExcelWriter writeHeadRow(Iterable rowData) { + Assert.isFalse(this.isClosed, "ExcelWriter has been closed!"); + RowUtil.writeRow(this.sheet.createRow(this.currentRow.getAndIncrement()), rowData, this.styleSet, true); + return this; + } + + /** + * 写出一行,根据rowBean数据类型不同,写出情况如下: + * + *
+	 * 1、如果为Iterable,直接写出一行
+	 * 2、如果为Map,isWriteKeyAsHead为true写出两行,Map的keys做为一行,values做为第二行,否则只写出一行values
+	 * 3、如果为Bean,转为Map写出,isWriteKeyAsHead为true写出两行,Map的keys做为一行,values做为第二行,否则只写出一行values
+	 * 
+ * + * @param rowBean 写出的Bean + * @param isWriteKeyAsHead 为true写出两行,Map的keys做为一行,values做为第二行,否则只写出一行values + * @return this + * @see #writeRow(Iterable) + * @see #writeRow(Map, boolean) + * @since 4.1.5 + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public ExcelWriter writeRow(Object rowBean, boolean isWriteKeyAsHead) { + if (rowBean instanceof Iterable) { + return writeRow((Iterable) rowBean); + } + Map rowMap = null; + if (rowBean instanceof Map) { + if (MapUtil.isNotEmpty(this.headerAlias)) { + rowMap = MapUtil.newTreeMap((Map) rowBean, getInitedAliasComparator()); + } else { + rowMap = (Map) rowBean; + } + } else if (BeanUtil.isBean(rowBean.getClass())) { + if (MapUtil.isEmpty(this.headerAlias)) { + rowMap = BeanUtil.beanToMap(rowBean, new LinkedHashMap(), false, false); + } else { + // 别名存在情况下按照别名的添加顺序排序Bean数据 + rowMap = BeanUtil.beanToMap(rowBean, new TreeMap(getInitedAliasComparator()), false, false); + } + } else { + // 其它转为字符串默认输出 + return writeRow(CollUtil.newArrayList(rowBean), isWriteKeyAsHead); + } + return writeRow(rowMap, isWriteKeyAsHead); + } + + /** + * 将一个Map写入到Excel,isWriteKeyAsHead为true写出两行,Map的keys做为一行,values做为第二行,否则只写出一行values
+ * 如果rowMap为空(包括null),则写出空行 + * + * @param rowMap 写出的Map,为空(包括null),则写出空行 + * @param isWriteKeyAsHead 为true写出两行,Map的keys做为一行,values做为第二行,否则只写出一行values + * @return this + */ + public ExcelWriter writeRow(Map rowMap, boolean isWriteKeyAsHead) { + Assert.isFalse(this.isClosed, "ExcelWriter has been closed!"); + if (MapUtil.isEmpty(rowMap)) { + // 如果写出数据为null或空,跳过当前行 + return passCurrentRow(); + } + + final Map aliasMap = aliasMap(rowMap); + + if (isWriteKeyAsHead) { + writeHeadRow(aliasMap.keySet()); + } + writeRow(aliasMap.values()); + return this; + } + + /** + * 写出一行数据
+ * 本方法只是将数据写入Workbook中的Sheet,并不写出到文件
+ * 写出的起始行为当前行号,可使用{@link #getCurrentRow()}方法调用,根据写出的的行数,当前行号自动+1
+ * 样式为默认样式,可使用{@link #getCellStyle()}方法调用后自定义默认样式 + * + * @param rowData 一行的数据 + * @return this + */ + public ExcelWriter writeRow(Iterable rowData) { + Assert.isFalse(this.isClosed, "ExcelWriter has been closed!"); + RowUtil.writeRow(this.sheet.createRow(this.currentRow.getAndIncrement()), rowData, this.styleSet, false); + return this; + } + + /** + * 给指定单元格赋值,使用默认单元格样式 + * + * @param x X坐标,从0计数,既列号 + * @param y Y坐标,从0计数,既行号 + * @param value 值 + * @return this + * @since 4.0.2 + */ + public ExcelWriter writeCellValue(int x, int y, Object value) { + final Cell cell = getOrCreateCell(x, y); + CellUtil.setCellValue(cell, value, this.styleSet, false); + return this; + } + + /** + * 为指定单元格创建样式 + * + * @param x X坐标,从0计数,既列号 + * @param y Y坐标,从0计数,既行号 + * @return {@link CellStyle} + * @since 4.0.9 + * @deprecated 请使用{@link #getOrCreateCellStyle(int, int)} + */ + @Deprecated + public CellStyle createStyleForCell(int x, int y) { + final Cell cell = getOrCreateCell(x, y); + final CellStyle cellStyle = this.workbook.createCellStyle(); + cell.setCellStyle(cellStyle); + return cellStyle; + } + + /** + * 创建字体 + * + * @return 字体 + * @since 4.1.0 + */ + public Font createFont() { + return getWorkbook().createFont(); + } + + /** + * 将Excel Workbook刷出到预定义的文件
+ * 如果用户未自定义输出的文件,将抛出{@link NullPointerException}
+ * 预定义文件可以通过{@link #setDestFile(File)} 方法预定义,或者通过构造定义 + * + * @return this + * @throws IORuntimeException IO异常 + */ + public ExcelWriter flush() throws IORuntimeException { + return flush(this.destFile); + } + + /** + * 将Excel Workbook刷出到文件
+ * 如果用户未自定义输出的文件,将抛出{@link NullPointerException} + * + * @param destFile 写出到的文件 + * @return this + * @throws IORuntimeException IO异常 + * @since 4.0.6 + */ + public ExcelWriter flush(File destFile) throws IORuntimeException { + Assert.notNull(destFile, "[destFile] is null, and you must call setDestFile(File) first or call flush(OutputStream)."); + return flush(FileUtil.getOutputStream(destFile), true); + } + + /** + * 将Excel Workbook刷出到输出流 + * + * @param out 输出流 + * @return this + * @throws IORuntimeException IO异常 + */ + public ExcelWriter flush(OutputStream out) throws IORuntimeException { + return flush(out, false); + } + + /** + * 将Excel Workbook刷出到输出流 + * + * @param out 输出流 + * @param isCloseOut 是否关闭输出流 + * @return this + * @throws IORuntimeException IO异常 + * @since 4.4.1 + */ + public ExcelWriter flush(OutputStream out, boolean isCloseOut) throws IORuntimeException { + Assert.isFalse(this.isClosed, "ExcelWriter has been closed!"); + try { + this.workbook.write(out); + out.flush(); + } catch (IOException e) { + throw new IORuntimeException(e); + } finally { + if (isCloseOut) { + IoUtil.close(out); + } + } + return this; + } + + /** + * 关闭工作簿
+ * 如果用户设定了目标文件,先写出目标文件后给关闭工作簿 + */ + @Override + public void close() { + if (null != this.destFile) { + flush(); + } + closeWithoutFlush(); + } + + /** + * 关闭工作簿但是不写出 + */ + protected void closeWithoutFlush() { + super.close(); + + // 清空对象 + this.currentRow = null; + this.styleSet = null; + } + + // -------------------------------------------------------------------------- Private method start + /** + * 为指定的key列表添加标题别名,如果没有定义key的别名,在onlyAlias为false时使用原key + * + * @param keys 键列表 + * @return 别名列表 + */ + private Map aliasMap(Map rowMap) { + if (MapUtil.isEmpty(this.headerAlias)) { + return rowMap; + } + + final Map filteredMap = new LinkedHashMap<>(); + String aliasName; + for (Entry entry : rowMap.entrySet()) { + aliasName = this.headerAlias.get(entry.getKey()); + if (null != aliasName) { + // 别名键值对加入 + filteredMap.put(aliasName, entry.getValue()); + } else if (false == this.onlyAlias) { + // 保留无别名设置的键值对 + filteredMap.put(entry.getKey(), entry.getValue()); + } + } + return filteredMap; + } + + /** + * 获取单例的别名比较器,比较器的顺序为别名加入的顺序 + * + * @return Comparator + * @since 4.1.5 + */ + private Comparator getInitedAliasComparator() { + if (MapUtil.isEmpty(this.headerAlias)) { + return null; + } + Comparator aliasComparator = this.aliasComparator; + if (null == aliasComparator) { + Set keySet = this.headerAlias.keySet(); + aliasComparator = new IndexedComparator<>(keySet.toArray(new String[keySet.size()])); + this.aliasComparator = aliasComparator; + } + return aliasComparator; + } + // -------------------------------------------------------------------------- Private method end +} diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/RowUtil.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/RowUtil.java new file mode 100644 index 000000000..39c7618de --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/RowUtil.java @@ -0,0 +1,85 @@ +package cn.hutool.poi.excel; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.poi.excel.cell.CellEditor; +import cn.hutool.poi.excel.cell.CellUtil; + +/** + * Excel中的行{@link Row}封装工具类 + * + * @author looly + * @since 4.0.7 + */ +public class RowUtil { + /** + * 获取已有行或创建新行 + * + * @param sheet Excel表 + * @param rowIndex 行号 + * @return {@link Row} + * @since 4.0.2 + */ + public static Row getOrCreateRow(Sheet sheet, int rowIndex) { + Row row = sheet.getRow(rowIndex); + if (null == row) { + row = sheet.createRow(rowIndex); + } + return row; + } + + /** + * 读取一行 + * + * @param row 行 + * @param cellEditor 单元格编辑器 + * @return 单元格值列表 + */ + public static List readRow(Row row, CellEditor cellEditor) { + if (null == row) { + return new ArrayList<>(0); + } + final short length = row.getLastCellNum(); + if (length < 0) { + return new ArrayList<>(0); + } + final List cellValues = new ArrayList<>((int) length); + Object cellValue; + boolean isAllNull = true; + for (short i = 0; i < length; i++) { + cellValue = CellUtil.getCellValue(row.getCell(i), cellEditor); + isAllNull &= StrUtil.isEmptyIfStr(cellValue); + cellValues.add(cellValue); + } + + if (isAllNull) { + // 如果每个元素都为空,则定义为空行 + return new ArrayList<>(0); + } + return cellValues; + } + + /** + * 写一行数据 + * + * @param row 行 + * @param rowData 一行的数据 + * @param styleSet 单元格样式集,包括日期等样式 + * @param isHeader 是否为标题行 + */ + public static void writeRow(Row row, Iterable rowData, StyleSet styleSet, boolean isHeader) { + int i = 0; + Cell cell; + for (Object value : rowData) { + cell = row.createCell(i); + CellUtil.setCellValue(cell, value, styleSet, isHeader); + i++; + } + } +} diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/StyleSet.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/StyleSet.java new file mode 100644 index 000000000..9a5e08587 --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/StyleSet.java @@ -0,0 +1,187 @@ +package cn.hutool.poi.excel; + +import java.io.Serializable; + +import org.apache.poi.ss.usermodel.BorderStyle; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.FillPatternType; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.IndexedColors; +import org.apache.poi.ss.usermodel.VerticalAlignment; +import org.apache.poi.ss.usermodel.Workbook; + +import cn.hutool.poi.excel.style.StyleUtil; + +/** + * 样式集合,此样式集合汇集了整个工作簿的样式,用于减少样式的创建和冗余 + * + * @author looly + * + */ +public class StyleSet implements Serializable{ + private static final long serialVersionUID = 1L; + + /** 工作簿引用 */ + private Workbook workbook; + /** 标题样式 */ + protected CellStyle headCellStyle; + /** 默认样式 */ + protected CellStyle cellStyle; + /** 默认数字样式 */ + protected CellStyle cellStyleForNumber; + /** 默认日期样式 */ + protected CellStyle cellStyleForDate; + + /** + * 构造 + * + * @param workbook 工作簿 + */ + public StyleSet(Workbook workbook) { + this.workbook = workbook; + this.headCellStyle = StyleUtil.createHeadCellStyle(workbook); + this.cellStyle = StyleUtil.createDefaultCellStyle(workbook); + + // 默认日期格式 + this.cellStyleForDate = StyleUtil.cloneCellStyle(workbook, this.cellStyle); + // 22表示:m/d/yy h:mm + this.cellStyleForDate.setDataFormat((short) 22); + + // 默认数字格式 + cellStyleForNumber = StyleUtil.cloneCellStyle(workbook, this.cellStyle); + // 2表示:0.00 + cellStyleForNumber.setDataFormat((short) 2); + } + + /** + * 获取头部样式,获取后可以定义整体头部样式 + * + * @return 头部样式 + */ + public CellStyle getHeadCellStyle() { + return headCellStyle; + } + + /** + * 获取常规单元格样式,获取后可以定义整体头部样式 + * + * @return 常规单元格样式 + */ + public CellStyle getCellStyle() { + return cellStyle; + } + + /** + * 获取数字(带小数点)单元格样式,获取后可以定义整体头部样式 + * + * @return 数字(带小数点)单元格样式 + */ + public CellStyle getCellStyleForNumber() { + return cellStyleForNumber; + } + + /** + * 获取日期单元格样式,获取后可以定义整体头部样式 + * + * @return 日期单元格样式 + */ + public CellStyle getCellStyleForDate() { + return cellStyleForDate; + } + + /** + * 定义所有单元格的边框类型 + * + * @param borderSize 边框粗细{@link BorderStyle}枚举 + * @param colorIndex 颜色的short值 + * @return this + * @since 4.0.0 + */ + public StyleSet setBorder(BorderStyle borderSize, IndexedColors colorIndex) { + StyleUtil.setBorder(this.headCellStyle, borderSize, colorIndex); + StyleUtil.setBorder(this.cellStyle, borderSize, colorIndex); + StyleUtil.setBorder(this.cellStyleForNumber, borderSize, colorIndex); + StyleUtil.setBorder(this.cellStyleForDate, borderSize, colorIndex); + return this; + } + + /** + * 设置cell文本对齐样式 + * + * @param halign 横向位置 + * @param valign 纵向位置 + * @return this + * @since 4.0.0 + */ + public StyleSet setAlign(HorizontalAlignment halign, VerticalAlignment valign) { + StyleUtil.setAlign(this.headCellStyle, halign, valign); + StyleUtil.setAlign(this.cellStyle, halign, valign); + StyleUtil.setAlign(this.cellStyleForNumber, halign, valign); + StyleUtil.setAlign(this.cellStyleForDate, halign, valign); + return this; + } + + /** + * 设置单元格背景样式 + * + * @param backgroundColor 背景色 + * @param withHeadCell 是否也定义头部样式 + * @return this + * @since 4.0.0 + */ + public StyleSet setBackgroundColor(IndexedColors backgroundColor, boolean withHeadCell) { + if (withHeadCell) { + StyleUtil.setColor(this.headCellStyle, backgroundColor, FillPatternType.SOLID_FOREGROUND); + } + StyleUtil.setColor(this.cellStyle, backgroundColor, FillPatternType.SOLID_FOREGROUND); + StyleUtil.setColor(this.cellStyleForNumber, backgroundColor, FillPatternType.SOLID_FOREGROUND); + StyleUtil.setColor(this.cellStyleForDate, backgroundColor, FillPatternType.SOLID_FOREGROUND); + return this; + } + + /** + * 设置全局字体 + * + * @param color 字体颜色 + * @param fontSize 字体大小,-1表示默认大小 + * @param fontName 字体名,null表示默认字体 + * @param ignoreHead 是否跳过头部样式 + * @return this + */ + public StyleSet setFont(short color, short fontSize, String fontName, boolean ignoreHead) { + final Font font = StyleUtil.createFont(this.workbook, color, fontSize, fontName); + return setFont(font, ignoreHead); + } + + /** + * 设置全局字体 + * + * @param font 字体,可以通过{@link StyleUtil#createFont(Workbook, short, short, String)}创建 + * @param ignoreHead 是否跳过头部样式 + * @return this + * @since 4.1.0 + */ + public StyleSet setFont(Font font, boolean ignoreHead) { + if (false == ignoreHead) { + this.headCellStyle.setFont(font); + } + this.cellStyle.setFont(font); + this.cellStyleForNumber.setFont(font); + this.cellStyleForDate.setFont(font); + return this; + } + + /** + * 设置单元格文本自动换行 + * + * @return this + * @since 4.5.16 + */ + public StyleSet setWrapText() { + this.cellStyle.setWrapText(true); + this.cellStyleForNumber.setWrapText(true); + this.cellStyleForDate.setWrapText(true); + return this; + } +} diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/WorkbookUtil.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/WorkbookUtil.java new file mode 100644 index 000000000..8638630cd --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/WorkbookUtil.java @@ -0,0 +1,287 @@ + +package cn.hutool.poi.excel; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.apache.poi.xssf.streaming.SXSSFWorkbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.poi.exceptions.POIException; + +/** + * Excel工作簿{@link Workbook}相关工具类 + * + * @author looly + * @since 4.0.7 + * + */ +public class WorkbookUtil { + + /** + * 创建或加载工作簿 + * + * @param excelFilePath Excel文件路径,绝对路径或相对于ClassPath路径 + * @return {@link Workbook} + * @since 3.1.1 + */ + public static Workbook createBook(String excelFilePath) { + return createBook(FileUtil.file(excelFilePath), null); + } + + /** + * 创建或加载工作簿 + * + * @param excelFile Excel文件 + * @return {@link Workbook} + */ + public static Workbook createBook(File excelFile) { + return createBook(excelFile, null); + } + + /** + * 创建工作簿,用于Excel写出 + * + *
+	 * 1. excelFile为null时直接返回一个空的工作簿,默认xlsx格式
+	 * 2. 文件已存在则通过流的方式读取到这个工作簿
+	 * 3. 文件不存在则检查传入文件路径是否以xlsx为扩展名,是则创建xlsx工作簿,否则创建xls工作簿
+	 * 
+ * + * @param excelFile Excel文件 + * @return {@link Workbook} + * @since 4.5.18 + */ + public static Workbook createBookForWriter(File excelFile) { + if (null == excelFile) { + return createBook(true); + } + + if (excelFile.exists()) { + return createBook(FileUtil.getInputStream(excelFile), true); + } + + return createBook(StrUtil.endWithIgnoreCase(excelFile.getName(), ".xlsx")); + } + + /** + * 创建或加载工作簿,只读模式 + * + * @param excelFile Excel文件 + * @param password Excel工作簿密码,如果无密码传{@code null} + * @return {@link Workbook} + */ + public static Workbook createBook(File excelFile, String password) { + try { + return WorkbookFactory.create(excelFile, password); + } catch (Exception e) { + throw new POIException(e); + } + } + + /** + * 创建或加载工作簿 + * + * @param in Excel输入流 + * @param closeAfterRead 读取结束是否关闭流 + * @return {@link Workbook} + */ + public static Workbook createBook(InputStream in, boolean closeAfterRead) { + return createBook(in, null, closeAfterRead); + } + + /** + * 创建或加载工作簿 + * + * @param in Excel输入流 + * @param password 密码 + * @param closeAfterRead 读取结束是否关闭流 + * @return {@link Workbook} + * @since 4.0.3 + */ + public static Workbook createBook(InputStream in, String password, boolean closeAfterRead) { + try { + return WorkbookFactory.create(IoUtil.toMarkSupportStream(in), password); + } catch (Exception e) { + throw new POIException(e); + } finally { + if (closeAfterRead) { + IoUtil.close(in); + } + } + } + + /** + * 根据文件类型创建新的工作簿,文件路径 + * + * @param isXlsx 是否为xlsx格式的Excel + * @return {@link Workbook} + * @since 4.1.0 + */ + public static Workbook createBook(boolean isXlsx) { + Workbook workbook; + if (isXlsx) { + workbook = new XSSFWorkbook(); + } else { + workbook = new org.apache.poi.hssf.usermodel.HSSFWorkbook(); + } + return workbook; + } + + /** + * 创建或加载SXSSFWorkbook工作簿 + * + * @param excelFilePath Excel文件路径,绝对路径或相对于ClassPath路径 + * @return {@link SXSSFWorkbook} + * @since 4.1.13 + */ + public static SXSSFWorkbook createSXSSFBook(String excelFilePath) { + return createSXSSFBook(FileUtil.file(excelFilePath), null); + } + + /** + * 创建或加载SXSSFWorkbook工作簿 + * + * @param excelFile Excel文件 + * @return {@link SXSSFWorkbook} + * @since 4.1.13 + */ + public static SXSSFWorkbook createSXSSFBook(File excelFile) { + return createSXSSFBook(excelFile, null); + } + + /** + * 创建或加载SXSSFWorkbook工作簿,只读模式 + * + * @param excelFile Excel文件 + * @param password Excel工作簿密码,如果无密码传{@code null} + * @return {@link SXSSFWorkbook} + * @since 4.1.13 + */ + public static SXSSFWorkbook createSXSSFBook(File excelFile, String password) { + return toSXSSFBook(createBook(excelFile, password)); + } + + /** + * 创建或加载SXSSFWorkbook工作簿 + * + * @param in Excel输入流 + * @param closeAfterRead 读取结束是否关闭流 + * @return {@link SXSSFWorkbook} + * @since 4.1.13 + */ + public static SXSSFWorkbook createSXSSFBook(InputStream in, boolean closeAfterRead) { + return createSXSSFBook(in, null, closeAfterRead); + } + + /** + * 创建或加载SXSSFWorkbook工作簿 + * + * @param in Excel输入流 + * @param password 密码 + * @param closeAfterRead 读取结束是否关闭流 + * @return {@link SXSSFWorkbook} + * @since 4.1.13 + */ + public static SXSSFWorkbook createSXSSFBook(InputStream in, String password, boolean closeAfterRead) { + return toSXSSFBook(createBook(in, password, closeAfterRead)); + } + + /** + * 创建SXSSFWorkbook,用于大批量数据写出 + * + * @return {@link SXSSFWorkbook} + * @since 4.1.13 + */ + public static SXSSFWorkbook createSXSSFBook() { + return new SXSSFWorkbook(); + } + + /** + * 创建SXSSFWorkbook,用于大批量数据写出 + * + * @param rowAccessWindowSize 在内存中的行数 + * @return {@link Workbook} + * @since 4.1.13 + */ + public static SXSSFWorkbook createSXSSFBook(int rowAccessWindowSize) { + return new SXSSFWorkbook(rowAccessWindowSize); + } + + /** + * 将Excel Workbook刷出到输出流,不关闭流 + * + * @param book {@link Workbook} + * @param out 输出流 + * @throws IORuntimeException IO异常 + * @since 3.2.0 + */ + public static void writeBook(Workbook book, OutputStream out) throws IORuntimeException { + try { + book.write(out); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 获取或者创建sheet表
+ * 如果sheet表在Workbook中已经存在,则获取之,否则创建之 + * + * @param book 工作簿{@link Workbook} + * @param sheetName 工作表名 + * @return 工作表{@link Sheet} + * @since 4.0.2 + */ + public static Sheet getOrCreateSheet(Workbook book, String sheetName) { + if (null == book) { + return null; + } + sheetName = StrUtil.isBlank(sheetName) ? "sheet1" : sheetName; + Sheet sheet = book.getSheet(sheetName); + if (null == sheet) { + sheet = book.createSheet(sheetName); + } + return sheet; + } + + /** + * + * sheet是否为空 + * + * @param sheet {@link Sheet} + * @return sheet是否为空 + * @since 4.0.1 + */ + public static boolean isEmpty(Sheet sheet) { + return null == sheet || (sheet.getLastRowNum() == 0 && sheet.getPhysicalNumberOfRows() == 0); + } + + // -------------------------------------------------------------------------------------------------------- Private method start + /** + * 将普通工作簿转换为SXSSFWorkbook + * + * @param book 工作簿 + * @return SXSSFWorkbook + * @since 4.1.13 + */ + private static SXSSFWorkbook toSXSSFBook(Workbook book) { + if (book instanceof SXSSFWorkbook) { + return (SXSSFWorkbook) book; + } + if (book instanceof XSSFWorkbook) { + return new SXSSFWorkbook((XSSFWorkbook) book); + } + throw new POIException("The input is not a [xlsx] format."); + } + // -------------------------------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/cell/CellEditor.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/cell/CellEditor.java new file mode 100644 index 000000000..53625c71c --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/cell/CellEditor.java @@ -0,0 +1,18 @@ +package cn.hutool.poi.excel.cell; + +import org.apache.poi.ss.usermodel.Cell; + +/** + * 单元格编辑器接口 + * @author Looly + * + */ +public interface CellEditor { + /** + * 编辑 + * @param cell 单元格对象,可以获取单元格行、列样式等信息 + * @param value 单元格值 + * @return 编辑后的对象 + */ + public Object edit(Cell cell, Object value); +} diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/cell/CellUtil.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/cell/CellUtil.java new file mode 100644 index 000000000..e39b1428d --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/cell/CellUtil.java @@ -0,0 +1,285 @@ +package cn.hutool.poi.excel.cell; + +import java.util.Calendar; +import java.util.Date; + +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.FormulaError; +import org.apache.poi.ss.usermodel.RichTextString; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.ss.util.RegionUtil; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.poi.excel.StyleSet; +import cn.hutool.poi.excel.editors.TrimEditor; + +/** + * Excel表格中单元格工具类 + * + * @author looly + * @since 4.0.7 + */ +public class CellUtil { + + /** + * 获取单元格值 + * + * @param cell {@link Cell}单元格 + * @param isTrimCellValue 如果单元格类型为字符串,是否去掉两边空白符 + * @return 值,类型可能为:Date、Double、Boolean、String + */ + public static Object getCellValue(Cell cell, boolean isTrimCellValue) { + if (null == cell) { + return null; + } + return getCellValue(cell, cell.getCellTypeEnum(), isTrimCellValue); + } + + /** + * 获取单元格值 + * + * @param cell {@link Cell}单元格 + * @param cellEditor 单元格值编辑器。可以通过此编辑器对单元格值做自定义操作 + * @return 值,类型可能为:Date、Double、Boolean、String + */ + public static Object getCellValue(Cell cell, CellEditor cellEditor) { + if (null == cell) { + return null; + } + return getCellValue(cell, cell.getCellTypeEnum(), cellEditor); + } + + /** + * 获取单元格值 + * + * @param cell {@link Cell}单元格 + * @param cellType 单元格值类型{@link CellType}枚举 + * @param isTrimCellValue 如果单元格类型为字符串,是否去掉两边空白符 + * @return 值,类型可能为:Date、Double、Boolean、String + */ + public static Object getCellValue(Cell cell, CellType cellType, final boolean isTrimCellValue) { + return getCellValue(cell, cellType, isTrimCellValue ? new TrimEditor() : null); + } + + /** + * 获取单元格值
+ * 如果单元格值为数字格式,则判断其格式中是否有小数部分,无则返回Long类型,否则返回Double类型 + * + * @param cell {@link Cell}单元格 + * @param cellType 单元格值类型{@link CellType}枚举,如果为{@code null}默认使用cell的类型 + * @param cellEditor 单元格值编辑器。可以通过此编辑器对单元格值做自定义操作 + * @return 值,类型可能为:Date、Double、Boolean、String + */ + public static Object getCellValue(Cell cell, CellType cellType, CellEditor cellEditor) { + if (null == cell) { + return null; + } + if (null == cellType) { + cellType = cell.getCellTypeEnum(); + } + + Object value; + switch (cellType) { + case NUMERIC: + value = getNumericValue(cell); + break; + case BOOLEAN: + value = cell.getBooleanCellValue(); + break; + case FORMULA: + // 遇到公式时查找公式结果类型 + value = getCellValue(cell, cell.getCachedFormulaResultTypeEnum(), cellEditor); + break; + case BLANK: + value = StrUtil.EMPTY; + break; + case ERROR: + final FormulaError error = FormulaError.forInt(cell.getErrorCellValue()); + value = (null == error) ? StrUtil.EMPTY : error.getString(); + break; + default: + value = cell.getStringCellValue(); + } + + return null == cellEditor ? value : cellEditor.edit(cell, value); + } + + /** + * 设置单元格值
+ * 根据传入的styleSet自动匹配样式
+ * 当为头部样式时默认赋值头部样式,但是头部中如果有数字、日期等类型,将按照数字、日期样式设置 + * + * @param cell 单元格 + * @param value 值 + * @param styleSet 单元格样式集,包括日期等样式 + * @param isHeader 是否为标题单元格 + */ + public static void setCellValue(Cell cell, Object value, StyleSet styleSet, boolean isHeader) { + final CellStyle headCellStyle = styleSet.getHeadCellStyle(); + final CellStyle cellStyle = styleSet.getCellStyle(); + if (isHeader && null != headCellStyle) { + cell.setCellStyle(headCellStyle); + } else if (null != cellStyle) { + cell.setCellStyle(cellStyle); + } + + if (null == value) { + cell.setCellValue(StrUtil.EMPTY); + } else if (value instanceof FormulaCellValue) { + // 公式 + cell.setCellFormula(((FormulaCellValue) value).getValue()); + } else if (value instanceof Date) { + if (null != styleSet && null != styleSet.getCellStyleForDate()) { + cell.setCellStyle(styleSet.getCellStyleForDate()); + } + cell.setCellValue((Date) value); + } else if (value instanceof Calendar) { + cell.setCellValue((Calendar) value); + } else if (value instanceof Boolean) { + cell.setCellValue((Boolean) value); + } else if (value instanceof RichTextString) { + cell.setCellValue((RichTextString) value); + } else if (value instanceof Number) { + if ((value instanceof Double || value instanceof Float) && null != styleSet && null != styleSet.getCellStyleForNumber()) { + cell.setCellStyle(styleSet.getCellStyleForNumber()); + } + cell.setCellValue(((Number) value).doubleValue()); + } else { + cell.setCellValue(value.toString()); + } + } + + /** + * 获取已有行或创建新行 + * + * @param row Excel表的行 + * @param cellIndex 列号 + * @return {@link Row} + * @since 4.0.2 + */ + public static Cell getOrCreateCell(Row row, int cellIndex) { + Cell cell = row.getCell(cellIndex); + if (null == cell) { + cell = row.createCell(cellIndex); + } + return cell; + } + + /** + * 判断指定的单元格是否是合并单元格 + * + * @param sheet {@link Sheet} + * @param row 行号 + * @param column 列号 + * @return 是否是合并单元格 + */ + public static boolean isMergedRegion(Sheet sheet, int row, int column) { + final int sheetMergeCount = sheet.getNumMergedRegions(); + CellRangeAddress ca; + for (int i = 0; i < sheetMergeCount; i++) { + ca = sheet.getMergedRegion(i); + if (row >= ca.getFirstRow() && row <= ca.getLastRow() && column >= ca.getFirstColumn() && column <= ca.getLastColumn()) { + return true; + } + } + return false; + } + + /** + * 合并单元格,可以根据设置的值来合并行和列 + * + * @param sheet 表对象 + * @param firstRow 起始行,0开始 + * @param lastRow 结束行,0开始 + * @param firstColumn 起始列,0开始 + * @param lastColumn 结束列,0开始 + * @param cellStyle 单元格样式,只提取边框样式 + * @return 合并后的单元格号 + */ + public static int mergingCells(Sheet sheet, int firstRow, int lastRow, int firstColumn, int lastColumn, CellStyle cellStyle) { + final CellRangeAddress cellRangeAddress = new CellRangeAddress(// + firstRow, // first row (0-based) + lastRow, // last row (0-based) + firstColumn, // first column (0-based) + lastColumn // last column (0-based) + ); + + if (null != cellStyle) { + RegionUtil.setBorderTop(cellStyle.getBorderTopEnum(), cellRangeAddress, sheet); + RegionUtil.setBorderRight(cellStyle.getBorderRightEnum(), cellRangeAddress, sheet); + RegionUtil.setBorderBottom(cellStyle.getBorderBottomEnum(), cellRangeAddress, sheet); + RegionUtil.setBorderLeft(cellStyle.getBorderLeftEnum(), cellRangeAddress, sheet); + } + return sheet.addMergedRegion(cellRangeAddress); + } + + // -------------------------------------------------------------------------------------------------------------- Private method start + /** + * 获取数字类型的单元格值 + * + * @param cell 单元格 + * @return 单元格值,可能为Long、Double、Date + */ + private static Object getNumericValue(Cell cell) { + final double value = cell.getNumericCellValue(); + + final CellStyle style = cell.getCellStyle(); + if (null == style) { + return value; + } + + final short formatIndex = style.getDataFormat(); + // 判断是否为日期 + if (isDateType(cell, formatIndex)) { + return DateUtil.date(cell.getDateCellValue());// 使用Hutool的DateTime包装 + } + + final String format = style.getDataFormatString(); + // 普通数字 + if (null != format && format.indexOf(StrUtil.C_DOT) < 0) { + final long longPart = (long) value; + if (longPart == value) { + // 对于无小数部分的数字类型,转为Long + return longPart; + } + } + return value; + } + + /** + * 是否为日期格式
+ * 判断方式: + * + *
+	 * 1、指定序号
+	 * 2、org.apache.poi.ss.usermodel.DateUtil.isADateFormat方法判定
+	 * 
+ * + * @param cell 单元格 + * @param formatIndex 格式序号 + * @return 是否为日期格式 + */ + private static boolean isDateType(Cell cell, int formatIndex) { + // yyyy-MM-dd----- 14 + // yyyy年m月d日---- 31 + // yyyy年m月------- 57 + // m月d日 ---------- 58 + // HH:mm----------- 20 + // h时mm分 -------- 32 + if (formatIndex == 14 || formatIndex == 31 || formatIndex == 57 || formatIndex == 58 || formatIndex == 20 || formatIndex == 32) { + return true; + } + + if (org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(cell)) { + return true; + } + + return false; + } + // -------------------------------------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/cell/CellValue.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/cell/CellValue.java new file mode 100644 index 000000000..5aacc1778 --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/cell/CellValue.java @@ -0,0 +1,17 @@ +package cn.hutool.poi.excel.cell; + +/** + * 抽象的单元格值接口,用于判断不同类型的单元格值 + * + * @param 值得类型 + * @author looly + * @since 4.0.11 + */ +public interface CellValue { + /** + * 获取单元格值 + * + * @return 值 + */ + T getValue(); +} diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/cell/FormulaCellValue.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/cell/FormulaCellValue.java new file mode 100644 index 000000000..4f6729f2b --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/cell/FormulaCellValue.java @@ -0,0 +1,23 @@ +package cn.hutool.poi.excel.cell; + +/** + * 公式类型的值 + * + * @author looly + * @since 4.0.11 + */ +public class FormulaCellValue implements CellValue { + + /** 公式 */ + String formula; + + public FormulaCellValue(String formula) { + this.formula = formula; + } + + @Override + public String getValue() { + return this.formula; + } + +} diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/cell/package-info.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/cell/package-info.java new file mode 100644 index 000000000..ca4ab939d --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/cell/package-info.java @@ -0,0 +1,6 @@ +/** + * Excel中单元格相关类,入口为CellUtil + * @author looly + * + */ +package cn.hutool.poi.excel.cell; \ No newline at end of file diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/editors/NumericToIntEditor.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/editors/NumericToIntEditor.java new file mode 100644 index 000000000..ef7a71932 --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/editors/NumericToIntEditor.java @@ -0,0 +1,22 @@ +package cn.hutool.poi.excel.editors; + +import org.apache.poi.ss.usermodel.Cell; + +import cn.hutool.poi.excel.cell.CellEditor; + +/** + * POI中NUMRIC类型的值默认返回的是Double类型,此编辑器用于转换其为int型 + * @author Looly + * + */ +public class NumericToIntEditor implements CellEditor{ + + @Override + public Object edit(Cell cell, Object value) { + if(value instanceof Number) { + return ((Number)value).intValue(); + } + return value; + } + +} diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/editors/TrimEditor.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/editors/TrimEditor.java new file mode 100644 index 000000000..a6b7822cf --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/editors/TrimEditor.java @@ -0,0 +1,23 @@ +package cn.hutool.poi.excel.editors; + +import org.apache.poi.ss.usermodel.Cell; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.poi.excel.cell.CellEditor; + +/** + * 去除String类型的单元格值两边的空格 + * @author Looly + * + */ +public class TrimEditor implements CellEditor{ + + @Override + public Object edit(Cell cell, Object value) { + if(value instanceof String) { + return StrUtil.trim((String)value); + } + return value; + } + +} diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/editors/package-info.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/editors/package-info.java new file mode 100644 index 000000000..3b8de82f9 --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/editors/package-info.java @@ -0,0 +1,6 @@ +/** + * 单元格值编辑器,内部使用 + * @author looly + * + */ +package cn.hutool.poi.excel.editors; \ No newline at end of file diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/package-info.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/package-info.java new file mode 100644 index 000000000..0cd590946 --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/package-info.java @@ -0,0 +1,7 @@ +/** + * POI中对Excel读写的封装,入口为ExcelUtil + * + * @author looly + * + */ +package cn.hutool.poi.excel; \ No newline at end of file diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/AbstractExcelSaxReader.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/AbstractExcelSaxReader.java new file mode 100644 index 000000000..ed3a245fe --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/AbstractExcelSaxReader.java @@ -0,0 +1,38 @@ +package cn.hutool.poi.excel.sax; + +import java.io.File; +import java.io.InputStream; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.poi.exceptions.POIException; + +/** + * 抽象的Sax方式Excel读取器,提供一些共用方法 + * + * @author looly + * + * @param 子对象类型,用于标记返回值this + * @since 3.2.0 + */ +public abstract class AbstractExcelSaxReader implements ExcelSaxReader { + + @Override + public T read(String path) throws POIException { + return read(FileUtil.file(path)); + } + + @Override + public T read(File file) throws POIException { + return read(file, -1); + } + + @Override + public T read(InputStream in) throws POIException { + return read(in, -1); + } + + @Override + public T read(String path, int sheetIndex) throws POIException { + return read(FileUtil.file(path), sheetIndex); + } +} diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/CellDataType.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/CellDataType.java new file mode 100644 index 000000000..fd7af1039 --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/CellDataType.java @@ -0,0 +1,73 @@ +package cn.hutool.poi.excel.sax; + +/** + * 单元格数据类型枚举 + * + * @author Looly + * + */ +public enum CellDataType { + /** Boolean类型 */ + BOOL("b"), + /** 类型错误 */ + ERROR("e"), + /** 计算结果类型 */ + FORMULA("str"), + /** 富文本类型 */ + INLINESTR("inlineStr"), + /** 字符串类型 */ + SSTINDEX("s"), + /** 数字类型 */ + NUMBER(""), + /** 日期类型 */ + DATE("m/d/yy"), + /** 空类型 */ + NULL(""); + + /** 属性值 */ + private String name; + + /** + * 构造 + * + * @param name 类型属性值 + */ + private CellDataType(String name) { + this.name = name; + } + + /** + * 获取对应类型的属性值 + * + * @return 属性值 + */ + public String getName() { + return name; + } + + /** + * 类型字符串转为枚举 + * @param name 类型字符串 + * @return 类型枚举 + */ + public static CellDataType of(String name) { + if(null == name) { + //默认数字 + return NUMBER; + } + + if(BOOL.name.equals(name)) { + return BOOL; + }else if(ERROR.name.equals(name)) { + return ERROR; + }else if(INLINESTR.name.equals(name)) { + return INLINESTR; + }else if(SSTINDEX.name.equals(name)) { + return SSTINDEX; + }else if(FORMULA.name.equals(name)) { + return FORMULA; + }else { + return NULL; + } + } +} diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel03SaxReader.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel03SaxReader.java new file mode 100644 index 000000000..f7fa973aa --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel03SaxReader.java @@ -0,0 +1,302 @@ +package cn.hutool.poi.excel.sax; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import org.apache.poi.hssf.eventusermodel.EventWorkbookBuilder.SheetRecordCollectingListener; +import org.apache.poi.hssf.eventusermodel.FormatTrackingHSSFListener; +import org.apache.poi.hssf.eventusermodel.HSSFEventFactory; +import org.apache.poi.hssf.eventusermodel.HSSFListener; +import org.apache.poi.hssf.eventusermodel.HSSFRequest; +import org.apache.poi.hssf.eventusermodel.MissingRecordAwareHSSFListener; +import org.apache.poi.hssf.eventusermodel.dummyrecord.LastCellOfRowDummyRecord; +import org.apache.poi.hssf.eventusermodel.dummyrecord.MissingCellDummyRecord; +import org.apache.poi.hssf.model.HSSFFormulaParser; +import org.apache.poi.hssf.record.BOFRecord; +import org.apache.poi.hssf.record.BlankRecord; +import org.apache.poi.hssf.record.BoolErrRecord; +import org.apache.poi.hssf.record.BoundSheetRecord; +import org.apache.poi.hssf.record.FormulaRecord; +import org.apache.poi.hssf.record.LabelRecord; +import org.apache.poi.hssf.record.LabelSSTRecord; +import org.apache.poi.hssf.record.NumberRecord; +import org.apache.poi.hssf.record.Record; +import org.apache.poi.hssf.record.SSTRecord; +import org.apache.poi.hssf.record.StringRecord; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.poifs.filesystem.POIFSFileSystem; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.poi.excel.sax.handler.RowHandler; +import cn.hutool.poi.exceptions.POIException; + +/** + * Excel2003格式的事件-用户模型方式读取器,在Hutool中,统一将此归类为Sax读取
+ * 参考:http://www.cnblogs.com/wshsdlau/p/5643862.html + * + * @author looly + * + */ +public class Excel03SaxReader extends AbstractExcelSaxReader implements HSSFListener { + + /** 如果为公式,true表示输出公式计算后的结果值,false表示输出公式本身 */ + private boolean isOutputFormulaValues = true; + + /** 用于解析公式 */ + private SheetRecordCollectingListener workbookBuildingListener; + /** 子工作簿,用于公式计算 */ + private HSSFWorkbook stubWorkbook; + + /** 静态字符串表 */ + private SSTRecord sstRecord; + + private FormatTrackingHSSFListener formatListener; + + /** Sheet边界记录,此Record中可以获得Sheet名 */ + private List boundSheetRecords = new ArrayList<>(); + + private boolean isOutputNextStringRecord; + + // 存储行记录的容器 + private List rowCellList = new ArrayList<>(); + + /** 自定义需要处理的sheet编号,如果-1表示处理所有sheet */ + private int rid = -1; + // 当前表索引 + private int curRid = -1; + + private RowHandler rowHandler; + + /** + * 构造 + * + * @param rowHandler 行处理器 + */ + public Excel03SaxReader(RowHandler rowHandler) { + this.rowHandler = rowHandler; + } + + // ------------------------------------------------------------------------------ Read start + @Override + public Excel03SaxReader read(File file, int rid) throws POIException { + try { + return read(new POIFSFileSystem(file), rid); + } catch (IOException e) { + throw new POIException(e); + } + } + + @Override + public Excel03SaxReader read(InputStream excelStream, int rid) throws POIException { + try { + return read(new POIFSFileSystem(excelStream), rid); + } catch (IOException e) { + throw new POIException(e); + } + } + + /** + * 读取 + * + * @param fs {@link POIFSFileSystem} + * @param rid sheet序号 + * @return this + * @throws POIException IO异常包装 + */ + public Excel03SaxReader read(POIFSFileSystem fs, int rid) throws POIException { + this.rid = rid; + + formatListener = new FormatTrackingHSSFListener(new MissingRecordAwareHSSFListener(this)); + final HSSFRequest request = new HSSFRequest(); + if (isOutputFormulaValues) { + request.addListenerForAllRecords(formatListener); + } else { + workbookBuildingListener = new SheetRecordCollectingListener(formatListener); + request.addListenerForAllRecords(workbookBuildingListener); + } + final HSSFEventFactory factory = new HSSFEventFactory(); + try { + factory.processWorkbookEvents(request, fs); + } catch (IOException e) { + throw new POIException(e); + } finally { + IoUtil.close(fs); + } + return this; + } + // ------------------------------------------------------------------------------ Read end + + /** + * 获得Sheet序号,如果处理所有sheet,获得最大的Sheet序号,从0开始 + * + * @return sheet序号 + */ + public int getSheetIndex() { + return this.rid; + } + + /** + * 获得Sheet名,如果处理所有sheet,获得后一个Sheet名,从0开始 + * + * @return Sheet名 + */ + public String getSheetName() { + if (this.boundSheetRecords.size() > this.rid) { + return this.boundSheetRecords.get(this.rid > -1 ? this.rid : this.curRid).getSheetname(); + } + return null; + } + + /** + * HSSFListener 监听方法,处理 Record + * + * @param record 记录 + */ + @Override + public void processRecord(Record record) { + if (this.rid > -1 && this.curRid > this.rid) { + // 指定Sheet之后的数据不再处理 + return; + } + + if (record instanceof BoundSheetRecord) { + // Sheet边界记录,此Record中可以获得Sheet名 + boundSheetRecords.add((BoundSheetRecord) record); + } else if (record instanceof SSTRecord) { + // 静态字符串表 + sstRecord = (SSTRecord) record; + } else if (record instanceof BOFRecord) { + BOFRecord bofRecord = (BOFRecord) record; + if (bofRecord.getType() == BOFRecord.TYPE_WORKSHEET) { + // 如果有需要,则建立子工作薄 + if (workbookBuildingListener != null && stubWorkbook == null) { + stubWorkbook = workbookBuildingListener.getStubHSSFWorkbook(); + } + curRid++; + } + } else if (isProcessCurrentSheet()) { + if (record instanceof MissingCellDummyRecord) { + // 空值的操作 + MissingCellDummyRecord mc = (MissingCellDummyRecord) record; + rowCellList.add(mc.getColumn(), StrUtil.EMPTY); + } else if (record instanceof LastCellOfRowDummyRecord) { + // 行结束 + processLastCell((LastCellOfRowDummyRecord) record); + } else { + // 处理单元格值 + processCellValue(record); + } + } + + } + + // ---------------------------------------------------------------------------------------------- Private method start + /** + * 处理单元格值 + * + * @param record 单元格 + */ + private void processCellValue(Record record) { + Object value = null; + + switch (record.getSid()) { + case BlankRecord.sid: + // 空白记录 + BlankRecord brec = (BlankRecord) record; + rowCellList.add(brec.getColumn(), StrUtil.EMPTY); + break; + case BoolErrRecord.sid: // 布尔类型 + BoolErrRecord berec = (BoolErrRecord) record; + rowCellList.add(berec.getColumn(), berec.getBooleanValue()); + break; + case FormulaRecord.sid: // 公式类型 + FormulaRecord frec = (FormulaRecord) record; + if (isOutputFormulaValues) { + if (Double.isNaN(frec.getValue())) { + // Formula result is a string + // This is stored in the next record + isOutputNextStringRecord = true; + } else { + value = formatListener.formatNumberDateCell(frec); + } + } else { + value = '"' + HSSFFormulaParser.toFormulaString(stubWorkbook, frec.getParsedExpression()) + '"'; + } + rowCellList.add(frec.getColumn(), value); + break; + case StringRecord.sid:// 单元格中公式的字符串 + if (isOutputNextStringRecord) { + // String for formula + StringRecord srec = (StringRecord) record; + value = srec.getString(); + isOutputNextStringRecord = false; + } + break; + case LabelRecord.sid: + LabelRecord lrec = (LabelRecord) record; + this.rowCellList.add(lrec.getColumn(), value); + break; + case LabelSSTRecord.sid: // 字符串类型 + LabelSSTRecord lsrec = (LabelSSTRecord) record; + if (sstRecord == null) { + rowCellList.add(lsrec.getColumn(), StrUtil.EMPTY); + } else { + value = sstRecord.getString(lsrec.getSSTIndex()).toString(); + rowCellList.add(lsrec.getColumn(), value); + } + break; + case NumberRecord.sid: // 数字类型 + NumberRecord numrec = (NumberRecord) record; + + final String formatString = formatListener.getFormatString(numrec); + if(formatString.contains(StrUtil.DOT)) { + //浮点数 + value = numrec.getValue(); + }else if(formatString.contains(StrUtil.SLASH) || formatString.contains(StrUtil.COLON)) { + //日期 + value = formatListener.formatNumberDateCell(numrec); + }else { + double numValue = numrec.getValue(); + final long longPart = (long) numValue; + // 对于无小数部分的数字类型,转为Long,否则保留原数字 + if(longPart == numValue) { + value = longPart; + }else { + value = numValue; + } + } + + // 向容器加入列值 + rowCellList.add(numrec.getColumn(), value); + break; + default: + break; + } + } + + /** + * 处理行结束后的操作,{@link LastCellOfRowDummyRecord}是行结束的标识Record + * + * @param lastCell 行结束的标识Record + */ + private void processLastCell(LastCellOfRowDummyRecord lastCell) { + // 每行结束时, 调用handle() 方法 + this.rowHandler.handle(curRid, lastCell.getRow(), this.rowCellList); + // 清空行Cache + this.rowCellList.clear(); + } + + /** + * 是否处理当前sheet + * + * @return 是否处理当前sheet + */ + private boolean isProcessCurrentSheet() { + return this.rid < 0 || this.curRid == this.rid; + } + // ---------------------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel07SaxReader.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel07SaxReader.java new file mode 100644 index 000000000..f61b3a6a1 --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel07SaxReader.java @@ -0,0 +1,381 @@ +package cn.hutool.poi.excel.sax; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.apache.poi.openxml4j.opc.OPCPackage; +import org.apache.poi.ss.usermodel.BuiltinFormats; +import org.apache.poi.xssf.eventusermodel.XSSFReader; +import org.apache.poi.xssf.model.SharedStringsTable; +import org.apache.poi.xssf.model.StylesTable; +import org.apache.poi.xssf.usermodel.XSSFCellStyle; +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.InputSource; +import org.xml.sax.Locator; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.XMLReaderFactory; + +import cn.hutool.core.exceptions.DependencyException; +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.poi.excel.sax.handler.RowHandler; +import cn.hutool.poi.exceptions.POIException; + +/** + * Sax方式读取Excel文件
+ * Excel2007格式说明见:http://www.cnblogs.com/wangmingshun/p/6654143.html + * + * @author Looly + * @since 3.1.2 + * + */ +public class Excel07SaxReader extends AbstractExcelSaxReader implements ContentHandler { + + // saxParser + private static final String CLASS_SAXPARSER = "org.apache.xerces.parsers.SAXParser"; + /** Cell单元格元素 */ + private static final String C_ELEMENT = "c"; + /** 行元素 */ + private static final String ROW_ELEMENT = "row"; + /** Cell中的行列号 */ + private static final String R_ATTR = "r"; + /** Cell类型 */ + private static final String T_ELEMENT = "t"; + /** SST(SharedStringsTable) 的索引 */ + private static final String S_ATTR_VALUE = "s"; + // 列中属性值 + private static final String T_ATTR_VALUE = "t"; + // sheet r:Id前缀 + private static final String RID_PREFIX = "rId"; + + // excel 2007 的共享字符串表,对应sharedString.xml + private SharedStringsTable sharedStringsTable; + // 当前行 + private int curRow; + // 当前列 + private int curCell; + // 上一次的内容 + private String lastContent; + // 单元数据类型 + private CellDataType cellDataType; + // 当前列坐标, 如A1,B5 + private String curCoordinate; + // 前一个列的坐标 + private String preCoordinate; + // 行的最大列坐标 + private String maxCellCoordinate; + // 单元格的格式表,对应style.xml + private StylesTable stylesTable; + // 单元格存储格式的索引,对应style.xml中的numFmts元素的子元素索引 + private int numFmtIndex; + // 单元格存储的格式化字符串,nmtFmt的formateCode属性的值 + private String numFmtString; + // sheet的索引 + private int sheetIndex; + + // 存储每行的列元素 + List rowCellList = new ArrayList<>(); + + /** 行处理器 */ + private RowHandler rowHandler; + + /** + * 构造 + * + * @param rowHandler 行处理器 + */ + public Excel07SaxReader(RowHandler rowHandler) { + this.rowHandler = rowHandler; + } + + /** + * 设置行处理器 + * + * @param rowHandler 行处理器 + * @return this + */ + public Excel07SaxReader setRowHandler(RowHandler rowHandler) { + this.rowHandler = rowHandler; + return this; + } + + // ------------------------------------------------------------------------------ Read start + @Override + public Excel07SaxReader read(File file, int rid) throws POIException { + try { + return read(OPCPackage.open(file), rid); + } catch (Exception e) { + throw new POIException(e); + } + } + + @Override + public Excel07SaxReader read(InputStream in, int rid) throws POIException { + try { + return read(OPCPackage.open(in), rid); + } catch (DependencyException e) { + throw e; + } catch (Exception e) { + throw ExceptionUtil.wrap(e, POIException.class); + } + } + + /** + * 开始读取Excel,Sheet编号从0开始计数 + * + * @param opcPackage {@link OPCPackage},Excel包 + * @param rid Excel中的sheet rid编号,如果为-1处理所有编号的sheet + * @return this + * @throws POIException POI异常 + */ + public Excel07SaxReader read(OPCPackage opcPackage, int rid) throws POIException { + InputStream sheetInputStream = null; + try { + final XSSFReader xssfReader = new XSSFReader(opcPackage); + + // 获取共享样式表 + stylesTable = xssfReader.getStylesTable(); + // 获取共享字符串表 + this.sharedStringsTable = xssfReader.getSharedStringsTable(); + + if (rid > -1) { + this.sheetIndex = rid; + // 根据 rId# 或 rSheet# 查找sheet + sheetInputStream = xssfReader.getSheet(RID_PREFIX + (rid + 1)); + parse(sheetInputStream); + } else { + this.sheetIndex = -1; + // 遍历所有sheet + final Iterator sheetInputStreams = xssfReader.getSheetsData(); + while (sheetInputStreams.hasNext()) { + // 重新读取一个sheet时行归零 + curRow = 0; + this.sheetIndex++; + sheetInputStream = sheetInputStreams.next(); + parse(sheetInputStream); + } + } + } catch (DependencyException e) { + throw e; + } catch (Exception e) { + throw ExceptionUtil.wrap(e, POIException.class); + } finally { + IoUtil.close(sheetInputStream); + IoUtil.close(opcPackage); + } + return this; + } + // ------------------------------------------------------------------------------ Read end + + /** + * 读到一个xml开始标签时的回调处理方法 + */ + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { + // 单元格元素 + if (C_ELEMENT.equals(qName)) { + + // 获取当前列坐标 + String tempCurCoordinate = attributes.getValue(R_ATTR); + // 前一列为null,则将其设置为"@",A为第一列,ascii码为65,前一列即为@,ascii码64 + if (preCoordinate == null) { + preCoordinate = String.valueOf(ExcelSaxUtil.CELL_FILL_CHAR); + } else { + // 存在,则前一列要设置为上一列的坐标 + preCoordinate = curCoordinate; + } + // 重置当前列 + curCoordinate = tempCurCoordinate; + // 设置单元格类型 + setCellType(attributes); + } + + lastContent = ""; + } + + /** + * 设置单元格的类型 + * + * @param attribute + */ + private void setCellType(Attributes attribute) { + // 重置numFmtIndex,numFmtString的值 + numFmtIndex = 0; + numFmtString = ""; + this.cellDataType = CellDataType.of(attribute.getValue(T_ATTR_VALUE)); + + // 获取单元格的xf索引,对应style.xml中cellXfs的子元素xf + final String xfIndexStr = attribute.getValue(S_ATTR_VALUE); + if (xfIndexStr != null) { + int xfIndex = Integer.parseInt(xfIndexStr); + XSSFCellStyle xssfCellStyle = stylesTable.getStyleAt(xfIndex); + numFmtIndex = xssfCellStyle.getDataFormat(); + numFmtString = xssfCellStyle.getDataFormatString(); + + if (numFmtString == null) { + numFmtString = BuiltinFormats.getBuiltinFormat(numFmtIndex); + } else if (CellDataType.NUMBER == this.cellDataType && org.apache.poi.ss.usermodel.DateUtil.isADateFormat(numFmtIndex, numFmtString)) { + cellDataType = CellDataType.DATE; + } + } + + } + + /** + * 标签结束的回调处理方法 + */ + @Override + public void endElement(String uri, String localName, String qName) throws SAXException { + final String contentStr = StrUtil.trim(lastContent); + + if (T_ELEMENT.equals(qName)) { + // type标签 + // rowCellList.add(curCell++, contentStr); + } else if (C_ELEMENT.equals(qName)) { + // cell标签 + Object value = ExcelSaxUtil.getDataValue(this.cellDataType, contentStr, this.sharedStringsTable, this.numFmtString); + // 补全单元格之间的空格 + fillBlankCell(preCoordinate, curCoordinate, false); + rowCellList.add(curCell++, value); + } else if (ROW_ELEMENT.equals(qName)) { + // 如果是row标签,说明已经到了一行的结尾 + // 最大列坐标以第一行的为准 + if (curRow == 0) { + maxCellCoordinate = curCoordinate; + } + + // 补全一行尾部可能缺失的单元格 + if (maxCellCoordinate != null) { + fillBlankCell(curCoordinate, maxCellCoordinate, true); + } + + rowHandler.handle(sheetIndex, curRow, rowCellList); + + // 一行结束 + // 清空rowCellList, + rowCellList.clear(); + // 行数增加 + curRow++; + // 当前列置0 + curCell = 0; + // 置空当前列坐标和前一列坐标 + curCoordinate = null; + preCoordinate = null; + } + } + + /** + * s标签结束的回调处理方法 + */ + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + // 得到单元格内容的值 + lastContent = lastContent.concat(new String(ch, start, length)); + } + + // --------------------------------------------------------------------------------------- Pass method start + @Override + public void setDocumentLocator(Locator locator) { + // pass + } + + /** + * ?xml标签的回调处理方法 + */ + @Override + public void startDocument() throws SAXException { + // pass + } + + @Override + public void endDocument() throws SAXException { + // pass + } + + @Override + public void startPrefixMapping(String prefix, String uri) throws SAXException { + // pass + } + + @Override + public void endPrefixMapping(String prefix) throws SAXException { + // pass + } + + @Override + public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException { + // pass + } + + @Override + public void processingInstruction(String target, String data) throws SAXException { + // pass + } + + @Override + public void skippedEntity(String name) throws SAXException { + // pass + } + // --------------------------------------------------------------------------------------- Pass method end + + // --------------------------------------------------------------------------------------- Private method start + /** + * 处理流中的Excel数据 + * + * @param sheetInputStream sheet流 + * @throws IOException IO异常 + * @throws SAXException SAX异常 + */ + private void parse(InputStream sheetInputStream) throws IOException, SAXException { + fetchSheetReader().parse(new InputSource(sheetInputStream)); + } + + /** + * 填充空白单元格,如果前一个单元格大于后一个,不需要填充
+ * + * @param preCoordinate 前一个单元格坐标 + * @param curCoordinate 当前单元格坐标 + * @param isEnd 是否为最后一个单元格 + */ + private void fillBlankCell(String preCoordinate, String curCoordinate, boolean isEnd) { + if (false == curCoordinate.equals(preCoordinate)) { + int len = ExcelSaxUtil.countNullCell(preCoordinate, curCoordinate); + if (isEnd) { + len++; + } + while (len-- > 0) { + rowCellList.add(curCell++, ""); + } + } + } + + /** + * 获取sheet的解析器 + * + * @param sharedStringsTable + * @return {@link XMLReader} + * @throws SAXException SAX异常 + */ + private XMLReader fetchSheetReader() throws SAXException { + XMLReader xmlReader = null; + try { + xmlReader = XMLReaderFactory.createXMLReader(CLASS_SAXPARSER); + } catch (SAXException e) { + if (e.getMessage().contains("org.apache.xerces.parsers.SAXParser")) { + throw new DependencyException(e, "You need to add 'xerces:xercesImpl' to your project and version >= 2.11.0"); + } else { + throw e; + } + } + xmlReader.setContentHandler(this); + return xmlReader; + } + // --------------------------------------------------------------------------------------- Private method end +} \ No newline at end of file diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/ExcelSaxReader.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/ExcelSaxReader.java new file mode 100644 index 000000000..58cbc330d --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/ExcelSaxReader.java @@ -0,0 +1,72 @@ +package cn.hutool.poi.excel.sax; + +import java.io.File; +import java.io.InputStream; + +import cn.hutool.poi.exceptions.POIException; + +/** + * Sax方式读取Excel接口,提供一些共用方法 + * @author looly + * + * @param 子对象类型,用于标记返回值this + * @since 3.2.0 + */ +public interface ExcelSaxReader { + /** + * 开始读取Excel,读取所有sheet + * + * @param path Excel文件路径 + * @return this + * @throws POIException POI异常 + */ + T read(String path) throws POIException; + + /** + * 开始读取Excel,读取所有sheet + * + * @param file Excel文件 + * @return this + * @throws POIException POI异常 + */ + T read(File file) throws POIException; + + /** + * 开始读取Excel,读取所有sheet,读取结束后并不关闭流 + * + * @param in Excel包流 + * @return this + * @throws POIException POI异常 + */ + T read(InputStream in) throws POIException; + + /** + * 开始读取Excel + * + * @param path 文件路径 + * @param rid Excel中的sheet rid编号,如果为-1处理所有编号的sheet + * @return this + * @throws POIException POI异常 + */ + T read(String path, int rid) throws POIException; + + /** + * 开始读取Excel + * + * @param file Excel文件 + * @param rid Excel中的sheet rid编号,如果为-1处理所有编号的sheet + * @return this + * @throws POIException POI异常 + */ + T read(File file, int rid) throws POIException; + + /** + * 开始读取Excel,读取结束后并不关闭流 + * + * @param in Excel流 + * @param rid Excel中的sheet rid编号,如果为-1处理所有编号的sheet + * @return this + * @throws POIException POI异常 + */ + T read(InputStream in, int rid) throws POIException; +} diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/ExcelSaxUtil.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/ExcelSaxUtil.java new file mode 100644 index 000000000..9ceb9acc8 --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/ExcelSaxUtil.java @@ -0,0 +1,159 @@ +package cn.hutool.poi.excel.sax; + +import org.apache.poi.ss.usermodel.DataFormatter; +import org.apache.poi.xssf.model.SharedStringsTable; +import org.apache.poi.xssf.usermodel.XSSFRichTextString; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; + +/** + * Sax方式读取Excel相关工具类 + * + * @author looly + * + */ +public class ExcelSaxUtil { + + // 填充字符串 + public static final char CELL_FILL_CHAR = '@'; + // 列的最大位数 + public static final int MAX_CELL_BIT = 3; + + /** + * 根据数据类型获取数据 + * + * @param cellDataType 数据类型枚举 + * @param value 数据值 + * @param sharedStringsTable {@link SharedStringsTable} + * @param numFmtString 数字格式名 + * @return 数据值 + */ + public static Object getDataValue(CellDataType cellDataType, String value, SharedStringsTable sharedStringsTable, String numFmtString) { + if (null == value) { + return null; + } + + if(null == cellDataType) { + cellDataType = CellDataType.NULL; + } + + Object result; + switch (cellDataType) { + case BOOL: + result = (value.charAt(0) != '0'); + break; + case ERROR: + result = StrUtil.format("\\\"ERROR: {} ", value); + break; + case FORMULA: + result = StrUtil.format("\"{}\"", value); + break; + case INLINESTR: + result = new XSSFRichTextString(value).toString(); + break; + case SSTINDEX: + try { + final int index = Integer.parseInt(value); + result = new XSSFRichTextString(sharedStringsTable.getEntryAt(index)).getString(); + } catch (NumberFormatException e) { + result = value; + } + break; + case NUMBER: + result = getNumberValue(value, numFmtString); + break; + case DATE: + try { + result = getDateValue(value); + } catch (Exception e) { + result = value; + } + break; + default: + result = value; + break; + } + return result; + } + + /** + * 格式化数字或日期值 + * + * @param value 值 + * @param numFmtIndex 数字格式索引 + * @param numFmtString 数字格式名 + * @return 格式化后的值 + */ + public static String formatCellContent(String value, int numFmtIndex, String numFmtString) { + if (null != numFmtString) { + try { + value = new DataFormatter().formatRawCellContents(Double.parseDouble(value), numFmtIndex, numFmtString); + } catch (NumberFormatException e) { + // ignore + } + } + return value; + } + + /** + * 计算两个单元格之间的单元格数目(同一行) + * + * @param preRef 前一个单元格位置,例如A1 + * @param ref 当前单元格位置,例如A8 + * @return 同一行中两个单元格之间的空单元格数 + */ + public static int countNullCell(String preRef, String ref) { + // excel2007最大行数是1048576,最大列数是16384,最后一列列名是XFD + // 数字代表列,去掉列信息 + String preXfd = StrUtil.nullToDefault(preRef, "@").replaceAll("\\d+", ""); + String xfd = StrUtil.nullToDefault(ref, "@").replaceAll("\\d+", ""); + + // A表示65,@表示64,如果A算作1,那@代表0 + // 填充最大位数3 + preXfd = StrUtil.fillBefore(preXfd, CELL_FILL_CHAR, MAX_CELL_BIT); + xfd = StrUtil.fillBefore(xfd, CELL_FILL_CHAR, MAX_CELL_BIT); + + char[] preLetter = preXfd.toCharArray(); + char[] letter = xfd.toCharArray(); + // 用字母表示则最多三位,每26个字母进一位 + int res = (letter[0] - preLetter[0]) * 26 * 26 + (letter[1] - preLetter[1]) * 26 + (letter[2] - preLetter[2]); + return res - 1; + } + + /** + * 获取日期 + * + * @param value 单元格值 + * @return 日期 + * @since 4.1.0 + */ + private static DateTime getDateValue(String value) { + return DateUtil.date(org.apache.poi.ss.usermodel.DateUtil.getJavaDate(Double.parseDouble(value), false)); + } + + /** + * 获取数字类型值 + * + * @param value 值 + * @param numFmtString 格式 + * @return 数字,可以是Double、Long + * @since 4.1.0 + */ + private static Number getNumberValue(String value, String numFmtString) { + if(StrUtil.isBlank(value)) { + return null; + } + double numValue = Double.parseDouble(value); + // 普通数字 + if (null != numFmtString && numFmtString.indexOf(StrUtil.C_DOT) < 0) { + final long longPart = (long) numValue; + if (longPart == numValue) { + // 对于无小数部分的数字类型,转为Long + return longPart; + } + } + return numValue; + } +} diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/handler/RowHandler.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/handler/RowHandler.java new file mode 100644 index 000000000..3c6ad3346 --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/handler/RowHandler.java @@ -0,0 +1,19 @@ +package cn.hutool.poi.excel.sax.handler; + +import java.util.List; + +/** + * Sax方式读取Excel行处理器 + * @author looly + * + */ +public interface RowHandler { + + /** + * 处理一行数据 + * @param sheetIndex 当前Sheet序号 + * @param rowIndex 当前行号 + * @param rowList 行数据列表 + */ + void handle(int sheetIndex, int rowIndex, List rowList); +} diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/handler/package-info.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/handler/package-info.java new file mode 100644 index 000000000..e5ddbe7b3 --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/handler/package-info.java @@ -0,0 +1,7 @@ +/** + * Sax读取中行处理器的定义和实现 + * + * @author looly + * + */ +package cn.hutool.poi.excel.sax.handler; \ No newline at end of file diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/package-info.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/package-info.java new file mode 100644 index 000000000..bdb3cb73c --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/sax/package-info.java @@ -0,0 +1,7 @@ +/** + * Sax方式操作Excel方式的封装 + * + * @author looly + * + */ +package cn.hutool.poi.excel.sax; \ No newline at end of file diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/style/Align.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/style/Align.java new file mode 100644 index 000000000..73080f3bf --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/style/Align.java @@ -0,0 +1,11 @@ +package cn.hutool.poi.excel.style; + +/** + * 对齐方式枚举 + * + * @author looly + * @since 4.1.0 + */ +public enum Align { + LEFT, RIGHT, CENTER +} diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/style/StyleUtil.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/style/StyleUtil.java new file mode 100644 index 000000000..450875259 --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/style/StyleUtil.java @@ -0,0 +1,178 @@ +package cn.hutool.poi.excel.style; + +import org.apache.poi.ss.usermodel.BorderStyle; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.FillPatternType; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.IndexedColors; +import org.apache.poi.ss.usermodel.VerticalAlignment; +import org.apache.poi.ss.usermodel.Workbook; + +import cn.hutool.core.util.StrUtil; + +/** + * Excel样式工具类 + * + * @author looly + * @since 4.0.0 + */ +public class StyleUtil { + + /** + * 克隆新的{@link CellStyle} + * + * @param cell 单元格 + * @param cellStyle 被复制的样式 + * @return {@link CellStyle} + */ + public static CellStyle cloneCellStyle(Cell cell, CellStyle cellStyle) { + return cloneCellStyle(cell.getSheet().getWorkbook(), cellStyle); + } + + /** + * 克隆新的{@link CellStyle} + * + * @param workbook 工作簿 + * @param cellStyle 被复制的样式 + * @return {@link CellStyle} + */ + public static CellStyle cloneCellStyle(Workbook workbook, CellStyle cellStyle) { + final CellStyle newCellStyle = workbook.createCellStyle(); + newCellStyle.cloneStyleFrom(cellStyle); + return newCellStyle; + } + + /** + * 设置cell文本对齐样式 + * + * @param cellStyle {@link CellStyle} + * @param halign 横向位置 + * @param valign 纵向位置 + * @return {@link CellStyle} + */ + public static CellStyle setAlign(CellStyle cellStyle, HorizontalAlignment halign, VerticalAlignment valign) { + cellStyle.setAlignment(halign); + cellStyle.setVerticalAlignment(valign); + return cellStyle; + } + + /** + * 设置cell的四个边框粗细和颜色 + * + * @param cellStyle {@link CellStyle} + * @param borderSize 边框粗细{@link BorderStyle}枚举 + * @param colorIndex 颜色的short值 + * @return {@link CellStyle} + */ + public static CellStyle setBorder(CellStyle cellStyle, BorderStyle borderSize, IndexedColors colorIndex) { + cellStyle.setBorderBottom(borderSize); + cellStyle.setBottomBorderColor(colorIndex.index); + + cellStyle.setBorderLeft(borderSize); + cellStyle.setLeftBorderColor(colorIndex.index); + + cellStyle.setBorderRight(borderSize); + cellStyle.setRightBorderColor(colorIndex.index); + + cellStyle.setBorderTop(borderSize); + cellStyle.setTopBorderColor(colorIndex.index); + + return cellStyle; + } + + /** + * 给cell设置颜色 + * + * @param cellStyle {@link CellStyle} + * @param color 背景颜色 + * @param fillPattern 填充方式 {@link FillPatternType}枚举 + * @return {@link CellStyle} + */ + public static CellStyle setColor(CellStyle cellStyle, IndexedColors color, FillPatternType fillPattern) { + return setColor(cellStyle, color.index, fillPattern); + } + + /** + * 给cell设置颜色 + * + * @param cellStyle {@link CellStyle} + * @param color 背景颜色 + * @param fillPattern 填充方式 {@link FillPatternType}枚举 + * @return {@link CellStyle} + */ + public static CellStyle setColor(CellStyle cellStyle, short color, FillPatternType fillPattern) { + cellStyle.setFillForegroundColor(color); + cellStyle.setFillPattern(fillPattern); + return cellStyle; + } + + /** + * 创建字体 + * + * @param workbook {@link Workbook} + * @param color 字体颜色 + * @param fontSize 字体大小 + * @param fontName 字体名称,可以为null使用默认字体 + * @return {@link Font} + */ + public static Font createFont(Workbook workbook, short color, short fontSize, String fontName) { + final Font font = workbook.createFont(); + return setFontStyle(font, color, fontSize, fontName); + } + + /** + * 设置字体样式 + * + * @param font 字体{@link Font} + * @param color 字体颜色 + * @param fontSize 字体大小 + * @param fontName 字体名称,可以为null使用默认字体 + * @return {@link Font} + */ + public static Font setFontStyle(Font font, short color, short fontSize, String fontName) { + if(color > 0) { + font.setColor(color); + } + if(fontSize > 0) { + font.setFontHeightInPoints(fontSize); + } + if(StrUtil.isNotBlank(fontName)) { + font.setFontName(fontName); + } + return font; + } + + /** + * 创建默认普通单元格样式 + * + *
+	 * 1. 文字上下左右居中
+	 * 2. 细边框,黑色
+	 * 
+ * + * @param workbook {@link Workbook} 工作簿 + * @return {@link CellStyle} + */ + public static CellStyle createDefaultCellStyle(Workbook workbook) { + final CellStyle cellStyle = workbook.createCellStyle(); + setAlign(cellStyle, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + setBorder(cellStyle, BorderStyle.THIN, IndexedColors.BLACK); + return cellStyle; + } + + /** + * 创建默认头部样式 + * + * @param workbook {@link Workbook} 工作簿 + * @return {@link CellStyle} + */ + public static CellStyle createHeadCellStyle(Workbook workbook) { + final CellStyle cellStyle = workbook.createCellStyle(); + setAlign(cellStyle, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + setBorder(cellStyle, BorderStyle.THIN, IndexedColors.BLACK); + setColor(cellStyle, IndexedColors.GREY_25_PERCENT, FillPatternType.SOLID_FOREGROUND); + return cellStyle; + } +} diff --git a/hutool-poi/src/main/java/cn/hutool/poi/excel/style/package-info.java b/hutool-poi/src/main/java/cn/hutool/poi/excel/style/package-info.java new file mode 100644 index 000000000..5e6649094 --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/excel/style/package-info.java @@ -0,0 +1,7 @@ +/** + * Excel样式封装,入口为:StyleUtil + * + * @author looly + * + */ +package cn.hutool.poi.excel.style; \ No newline at end of file diff --git a/hutool-poi/src/main/java/cn/hutool/poi/exceptions/POIException.java b/hutool-poi/src/main/java/cn/hutool/poi/exceptions/POIException.java new file mode 100644 index 000000000..959687c85 --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/exceptions/POIException.java @@ -0,0 +1,32 @@ +package cn.hutool.poi.exceptions; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.StrUtil; + +/** + * POI异常 + * @author xiaoleilu + */ +public class POIException extends RuntimeException{ + private static final long serialVersionUID = 2711633732613506552L; + + public POIException(Throwable e) { + super(ExceptionUtil.getMessage(e), e); + } + + public POIException(String message) { + super(message); + } + + public POIException(String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params)); + } + + public POIException(String message, Throwable throwable) { + super(message, throwable); + } + + public POIException(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } +} diff --git a/hutool-poi/src/main/java/cn/hutool/poi/exceptions/package-info.java b/hutool-poi/src/main/java/cn/hutool/poi/exceptions/package-info.java new file mode 100644 index 000000000..41b6465a4 --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/exceptions/package-info.java @@ -0,0 +1,7 @@ +/** + * POI相关异常 + * + * @author looly + * + */ +package cn.hutool.poi.exceptions; \ No newline at end of file diff --git a/hutool-poi/src/main/java/cn/hutool/poi/package-info.java b/hutool-poi/src/main/java/cn/hutool/poi/package-info.java new file mode 100644 index 000000000..2faf574e8 --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/package-info.java @@ -0,0 +1,9 @@ +/** + * POI封装实现
+ * Java针对MS Office的操作的库屈指可数,比较有名的就是Apache的POI库。
+ * 这个库异常强大,但是使用起来也并不容易。Hutool针对POI封装一些常用工具,使Java操作Excel等文件变得异常简单。 + * + * @author looly + * + */ +package cn.hutool.poi; \ No newline at end of file diff --git a/hutool-poi/src/main/java/cn/hutool/poi/word/DocUtil.java b/hutool-poi/src/main/java/cn/hutool/poi/word/DocUtil.java new file mode 100644 index 000000000..7eda1e513 --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/word/DocUtil.java @@ -0,0 +1,37 @@ +package cn.hutool.poi.word; + +import java.io.File; +import java.io.IOException; + +import org.apache.poi.openxml4j.exceptions.InvalidFormatException; +import org.apache.poi.openxml4j.opc.OPCPackage; +import org.apache.poi.xwpf.usermodel.XWPFDocument; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.poi.exceptions.POIException; + +/** + * Word Document工具 + * + * @author looly + * @since 4.4.1 + */ +public class DocUtil { + + /** + * 创建{@link XWPFDocument},如果文件已存在则读取之,否则创建新的 + * + * @param file docx文件 + * @return {@link XWPFDocument} + */ + public static XWPFDocument create(File file) { + try { + return FileUtil.exist(file) ? new XWPFDocument(OPCPackage.open(file)) : new XWPFDocument(); + } catch (InvalidFormatException e) { + throw new POIException(e); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } +} diff --git a/hutool-poi/src/main/java/cn/hutool/poi/word/TableUtil.java b/hutool-poi/src/main/java/cn/hutool/poi/word/TableUtil.java new file mode 100644 index 000000000..7ed2f7400 --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/word/TableUtil.java @@ -0,0 +1,153 @@ +package cn.hutool.poi.word; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.apache.poi.xwpf.usermodel.XWPFDocument; +import org.apache.poi.xwpf.usermodel.XWPFTable; +import org.apache.poi.xwpf.usermodel.XWPFTableCell; +import org.apache.poi.xwpf.usermodel.XWPFTableRow; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.IterUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; + +/** + * Word中表格相关工具 + * + * @author Looly + * @since 4.5.14 + */ +public class TableUtil { + + /** + * 创建空表,只有一行 + * + * @param doc {@link XWPFDocument} + * @return {@link XWPFTable} + */ + public static XWPFTable createTable(XWPFDocument doc) { + return createTable(doc, null); + } + + /** + * 创建表格并填充数据 + * + * @param doc {@link XWPFDocument} + * @param data 数据 + * @return {@link XWPFTable} + */ + public static XWPFTable createTable(XWPFDocument doc, Iterable data) { + Assert.notNull(doc, "XWPFDocument must be not null !"); + XWPFTable table = doc.createTable(); + + if (IterUtil.isEmpty(data)) { + // 数据为空,返回空表 + return table; + } + + int index = 0; + for (Object rowData : data) { + writeRow(getOrCreateRow(table, index), rowData, true); + index ++; + } + + return table; + } + + /** + * 写一行数据 + * + * @param row 行 + * @param rowBean 行数据 + * @param isWriteKeyAsHead 如果为Map或者Bean,是否写标题 + */ + @SuppressWarnings("rawtypes") + public static void writeRow(XWPFTableRow row, Object rowBean, boolean isWriteKeyAsHead) { + if (rowBean instanceof Iterable) { + writeRow(row, (Iterable) rowBean); + } + + Map rowMap = null; + if(rowBean instanceof Map) { + rowMap = (Map) rowBean; + } else if (BeanUtil.isBean(rowBean.getClass())) { + rowMap = BeanUtil.beanToMap(rowBean, new LinkedHashMap(), false, false); + } else { + // 其它转为字符串默认输出 + writeRow(row, CollUtil.newArrayList(rowBean), isWriteKeyAsHead); + } + + writeRow(row, rowMap, isWriteKeyAsHead); + } + + /** + * 写行数据 + * + * @param row 行 + * @param rowMap 行数据 + * @param isWriteKeyAsHead 是否写标题 + */ + public static void writeRow(XWPFTableRow row, Map rowMap, boolean isWriteKeyAsHead) { + if (MapUtil.isEmpty(rowMap)) { + return; + } + + if (isWriteKeyAsHead) { + writeRow(row, rowMap.keySet()); + } + writeRow(row, rowMap.values()); + } + + /** + * 写行数据 + * + * @param row 行 + * @param rowData 行数据 + */ + public static void writeRow(XWPFTableRow row, Iterable rowData) { + XWPFTableCell cell; + int index = 0; + for (Object cellData : rowData) { + cell = getOrCreateCell(row, index); + cell.setText(Convert.toStr(cellData)); + index++; + } + } + + /** + * 获取或创建新行
+ * 存在则直接返回,不存在创建新的行 + * + * @param table {@link XWPFTable} + * @param index 索引(行号),从0开始 + * @return {@link XWPFTableRow} + */ + public static XWPFTableRow getOrCreateRow(XWPFTable table, int index) { + XWPFTableRow row = table.getRow(index); + if (null == row) { + row = table.createRow(); + } + + return row; + } + + /** + * 获取或创建新单元格
+ * 存在则直接返回,不存在创建新的单元格 + * + * @param row {@link XWPFTableRow} 行 + * @param index index 索引(列号),从0开始 + * @return {@link XWPFTableCell} + */ + public static XWPFTableCell getOrCreateCell(XWPFTableRow row, int index) { + XWPFTableCell cell = row.getCell(index); + if (null == cell) { + cell = row.createCell(); + } + return cell; + } +} diff --git a/hutool-poi/src/main/java/cn/hutool/poi/word/Word07Writer.java b/hutool-poi/src/main/java/cn/hutool/poi/word/Word07Writer.java new file mode 100644 index 000000000..d224de536 --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/word/Word07Writer.java @@ -0,0 +1,220 @@ +package cn.hutool.poi.word; + +import java.awt.Font; +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; + +import org.apache.poi.xwpf.usermodel.ParagraphAlignment; +import org.apache.poi.xwpf.usermodel.XWPFDocument; +import org.apache.poi.xwpf.usermodel.XWPFParagraph; +import org.apache.poi.xwpf.usermodel.XWPFRun; + +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.ArrayUtil; + +/** + * Word生成器 + * + * @author looly + * @since 4.4.1 + */ +public class Word07Writer implements Closeable { + + private XWPFDocument doc; + /** 目标文件 */ + protected File destFile; + /** 是否被关闭 */ + protected boolean isClosed; + + // -------------------------------------------------------------------------- Constructor start + public Word07Writer() { + this(new XWPFDocument()); + } + + /** + * 构造 + * + * @param destFile 写出的文件 + */ + public Word07Writer(File destFile) { + this(DocUtil.create(destFile), destFile); + } + + /** + * 构造 + * + * @param doc {@link XWPFDocument} + */ + public Word07Writer(XWPFDocument doc) { + this(doc, null); + } + + /** + * 构造 + * + * @param doc {@link XWPFDocument} + * @param destFile 写出的文件 + */ + public Word07Writer(XWPFDocument doc, File destFile) { + this.doc = doc; + this.destFile = destFile; + } + + // -------------------------------------------------------------------------- Constructor end + + /** + * 获取{@link XWPFDocument} + * + * @return {@link XWPFDocument} + */ + public XWPFDocument getDoc() { + return this.doc; + } + + /** + * 设置写出的目标文件 + * + * @param destFile 目标文件 + * @return this + */ + public Word07Writer setDestFile(File destFile) { + this.destFile = destFile; + return this; + } + + /** + * 增加一个段落 + * + * @param font 字体信息{@link Font} + * @param texts 段落中的文本,支持多个文本作为一个段落 + * @return this + */ + public Word07Writer addText(Font font, String... texts) { + return addText(null, font, texts); + } + + /** + * 增加一个段落 + * + * @param align 段落对齐方式{@link ParagraphAlignment} + * @param font 字体信息{@link Font} + * @param texts 段落中的文本,支持多个文本作为一个段落 + * @return this + */ + public Word07Writer addText(ParagraphAlignment align, Font font, String... texts) { + final XWPFParagraph p = this.doc.createParagraph(); + if (null != align) { + p.setAlignment(align); + } + if (ArrayUtil.isNotEmpty(texts)) { + XWPFRun run; + for (String text : texts) { + run = p.createRun(); + run.setText(text); + if (null != font) { + run.setFontFamily(font.getFamily()); + run.setFontSize(font.getSize()); + run.setBold(font.isBold()); + run.setItalic(font.isItalic()); + } + } + } + return this; + } + + /** + * 增加表格数据 + * + * @param data 表格数据,多行数据。元素表示一行数据,当为集合或者数组时,为一行;当为Map或者Bean时key表示标题,values为数据 + * @return this + * @since 4.5.16 + */ + public Word07Writer addTable(Iterable data) { + TableUtil.createTable(this.doc, data); + return this; + } + + /** + * 将Excel Workbook刷出到预定义的文件
+ * 如果用户未自定义输出的文件,将抛出{@link NullPointerException}
+ * 预定义文件可以通过{@link #setDestFile(File)} 方法预定义,或者通过构造定义 + * + * @return this + * @throws IORuntimeException IO异常 + */ + public Word07Writer flush() throws IORuntimeException { + return flush(this.destFile); + } + + /** + * 将Excel Workbook刷出到文件
+ * 如果用户未自定义输出的文件,将抛出{@link NullPointerException} + * + * @param destFile 写出到的文件 + * @return this + * @throws IORuntimeException IO异常 + */ + public Word07Writer flush(File destFile) throws IORuntimeException { + Assert.notNull(destFile, "[destFile] is null, and you must call setDestFile(File) first or call flush(OutputStream)."); + return flush(FileUtil.getOutputStream(destFile), true); + } + + /** + * 将Word Workbook刷出到输出流 + * + * @param out 输出流 + * @return this + * @throws IORuntimeException IO异常 + */ + public Word07Writer flush(OutputStream out) throws IORuntimeException { + return flush(out, false); + } + + /** + * 将Word Document刷出到输出流 + * + * @param out 输出流 + * @param isCloseOut 是否关闭输出流 + * @return this + * @throws IORuntimeException IO异常 + */ + public Word07Writer flush(OutputStream out, boolean isCloseOut) throws IORuntimeException { + Assert.isFalse(this.isClosed, "WordWriter has been closed!"); + try { + this.doc.write(out); + out.flush(); + } catch (IOException e) { + throw new IORuntimeException(e); + } finally { + if (isCloseOut) { + IoUtil.close(out); + } + } + return this; + } + + /** + * 关闭Word文档
+ * 如果用户设定了目标文件,先写出目标文件后给关闭工作簿 + */ + @Override + public void close() { + if (null != this.destFile) { + flush(); + } + closeWithoutFlush(); + } + + /** + * 关闭Word文档但是不写出 + */ + protected void closeWithoutFlush() { + IoUtil.close(this.doc); + this.isClosed = true; + } +} diff --git a/hutool-poi/src/main/java/cn/hutool/poi/word/WordUtil.java b/hutool-poi/src/main/java/cn/hutool/poi/word/WordUtil.java new file mode 100644 index 000000000..a20931069 --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/word/WordUtil.java @@ -0,0 +1,30 @@ +package cn.hutool.poi.word; + +import java.io.File; + +/** + * Word工具类 + * + * @author Looly + * @since 4.5.16 + */ +public class WordUtil { + /** + * 创建Word 07格式的生成器 + * + * @return {@link Word07Writer} + */ + public static Word07Writer getWriter() { + return new Word07Writer(); + } + + /** + * 创建Word 07格式的生成器 + * + * @param destFile 目标文件 + * @return {@link Word07Writer} + */ + public static Word07Writer getWriter(File destFile) { + return new Word07Writer(destFile); + } +} diff --git a/hutool-poi/src/main/java/cn/hutool/poi/word/package-info.java b/hutool-poi/src/main/java/cn/hutool/poi/word/package-info.java new file mode 100644 index 000000000..8ad5af389 --- /dev/null +++ b/hutool-poi/src/main/java/cn/hutool/poi/word/package-info.java @@ -0,0 +1,7 @@ +/** + * POI中对Word操作封装 + * + * @author looly + * + */ +package cn.hutool.poi.word; \ No newline at end of file diff --git a/hutool-poi/src/test/java/cn/hutool/poi/excel/test/BigExcelWriteTest.java b/hutool-poi/src/test/java/cn/hutool/poi/excel/test/BigExcelWriteTest.java new file mode 100644 index 000000000..a3e223647 --- /dev/null +++ b/hutool-poi/src/test/java/cn/hutool/poi/excel/test/BigExcelWriteTest.java @@ -0,0 +1,209 @@ +/** + * + */ +package cn.hutool.poi.excel.test; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.FillPatternType; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.IndexedColors; +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.poi.excel.BigExcelWriter; +import cn.hutool.poi.excel.ExcelUtil; +import cn.hutool.poi.excel.style.StyleUtil; + +/** + * 写出Excel单元测试 + * + * @author looly + */ +public class BigExcelWriteTest { + + @Test + @Ignore + public void writeTest2() { + List row = CollUtil.newArrayList("姓名", "加班日期", "下班时间", "加班时长", "餐补", "车补次数", "车补", "总计"); + BigExcelWriter overtimeWriter = ExcelUtil.getBigWriter("e:/excel/single_line.xlsx"); + overtimeWriter.write(row); + overtimeWriter.close(); + } + + @Test + @Ignore + public void writeTest() { + List row1 = CollUtil.newArrayList("aaaaa", "bb", "cc", "dd", DateUtil.date(), 3.22676575765); + List row2 = CollUtil.newArrayList("aa1", "bb1", "cc1", "dd1", DateUtil.date(), 250.7676); + List row3 = CollUtil.newArrayList("aa2", "bb2", "cc2", "dd2", DateUtil.date(), 0.111); + List row4 = CollUtil.newArrayList("aa3", "bb3", "cc3", "dd3", DateUtil.date(), 35); + List row5 = CollUtil.newArrayList("aa4", "bb4", "cc4", "dd4", DateUtil.date(), 28.00); + + List> rows = CollUtil.newArrayList(row1, row2, row3, row4, row5); + for(int i=0; i < 400000; i++) { + //超大列表写出测试 + rows.add(ObjectUtil.clone(row1)); + } + + String filePath = "e:/bigWriteTest.xlsx"; + FileUtil.del(filePath); + // 通过工具类创建writer + BigExcelWriter writer = ExcelUtil.getBigWriter(filePath); + +// // 跳过当前行,既第一行,非必须,在此演示用 +// writer.passCurrentRow(); +// // 合并单元格后的标题行,使用默认标题样式 +// writer.merge(row1.size() - 1, "大数据测试标题"); + // 一次性写出内容,使用默认样式 + writer.write(rows); +// writer.autoSizeColumn(0, true); + // 关闭writer,释放内存 + writer.close(); + } + + @Test + @Ignore + public void mergeTest() { + List row1 = CollUtil.newArrayList("aa", "bb", "cc", "dd", DateUtil.date(), 3.22676575765); + List row2 = CollUtil.newArrayList("aa1", "bb1", "cc1", "dd1", DateUtil.date(), 250.7676); + List row3 = CollUtil.newArrayList("aa2", "bb2", "cc2", "dd2", DateUtil.date(), 0.111); + List row4 = CollUtil.newArrayList("aa3", "bb3", "cc3", "dd3", DateUtil.date(), 35); + List row5 = CollUtil.newArrayList("aa4", "bb4", "cc4", "dd4", DateUtil.date(), 28.00); + + List> rows = CollUtil.newArrayList(row1, row2, row3, row4, row5); + + // 通过工具类创建writer + BigExcelWriter writer = ExcelUtil.getBigWriter("e:/mergeTest.xlsx"); + CellStyle style = writer.getStyleSet().getHeadCellStyle(); + StyleUtil.setColor(style, IndexedColors.RED, FillPatternType.SOLID_FOREGROUND); + + // 跳过当前行,既第一行,非必须,在此演示用 + writer.passCurrentRow(); + // 合并单元格后的标题行,使用默认标题样式 + writer.merge(row1.size() - 1, "测试标题"); + // 一次性写出内容,使用默认样式 + writer.write(rows); + + // 合并单元格后的标题行,使用默认标题样式 + writer.merge(7, 10, 4, 10, "测试Merge", false); + + // 关闭writer,释放内存 + writer.close(); + } + + @Test + @Ignore + public void writeMapTest() { + Map row1 = new LinkedHashMap<>(); + row1.put("姓名", "张三"); + row1.put("年龄", 23); + row1.put("成绩", 88.32); + row1.put("是否合格", true); + row1.put("考试日期", DateUtil.date()); + + Map row2 = new LinkedHashMap<>(); + row2.put("姓名", "李四"); + row2.put("年龄", 33); + row2.put("成绩", 59.50); + row2.put("是否合格", false); + row2.put("考试日期", DateUtil.date()); + + ArrayList> rows = CollUtil.newArrayList(row1, row2); + + // 通过工具类创建writer + String path = "e:/bigWriteMapTest.xlsx"; + FileUtil.del(path); + BigExcelWriter writer = ExcelUtil.getBigWriter(path); + + //设置内容字体 + Font font = writer.createFont(); + font.setBold(true); + font.setColor(Font.COLOR_RED); + font.setItalic(true); + writer.getStyleSet().setFont(font, true); + + // 合并单元格后的标题行,使用默认标题样式 + writer.merge(row1.size() - 1, "一班成绩单"); + // 一次性写出内容,使用默认样式 + writer.write(rows); + // 关闭writer,释放内存 + writer.close(); + } + + @Test + @Ignore + public void writeMapTest2() { + Map row1 = MapUtil.newHashMap(true); + row1.put("姓名", "张三"); + row1.put("年龄", 23); + row1.put("成绩", 88.32); + row1.put("是否合格", true); + row1.put("考试日期", DateUtil.date()); + + // 通过工具类创建writer + String path = "e:/bigWriteMapTest2.xlsx"; + FileUtil.del(path); + BigExcelWriter writer = ExcelUtil.getBigWriter(path); + + // 一次性写出内容,使用默认样式 + writer.writeRow(row1, true); + // 关闭writer,释放内存 + writer.close(); + } + + @Test + @Ignore + public void writeBeanTest() { + TestBean bean1 = new TestBean(); + bean1.setName("张三"); + bean1.setAge(22); + bean1.setPass(true); + bean1.setScore(66.30); + bean1.setExamDate(DateUtil.date()); + + TestBean bean2 = new TestBean(); + bean2.setName("李四"); + bean2.setAge(28); + bean2.setPass(false); + bean2.setScore(38.50); + bean2.setExamDate(DateUtil.date()); + + List rows = CollUtil.newArrayList(bean1, bean2); + // 通过工具类创建writer + String file = "e:/bigWriteBeanTest.xlsx"; + FileUtil.del(file); + BigExcelWriter writer = ExcelUtil.getBigWriter(file); + //自定义标题 + writer.addHeaderAlias("name", "姓名"); + writer.addHeaderAlias("age", "年龄"); + writer.addHeaderAlias("score", "分数"); + writer.addHeaderAlias("isPass", "是否通过"); + writer.addHeaderAlias("examDate", "考试时间"); + // 合并单元格后的标题行,使用默认标题样式 + writer.merge(4, "一班成绩单"); + // 一次性写出内容,使用默认样式 + writer.write(rows); + // 关闭writer,释放内存 + writer.close(); + } + + @Test + @Ignore + public void writeCellValueTest() { + String path = "e:/cellValueTest.xlsx"; + FileUtil.del(path); + BigExcelWriter writer = new BigExcelWriter(path); + writer.writeCellValue(3, 5, "aaa"); + writer.close(); + } +} diff --git a/hutool-poi/src/test/java/cn/hutool/poi/excel/test/CellUtilTest.java b/hutool-poi/src/test/java/cn/hutool/poi/excel/test/CellUtilTest.java new file mode 100644 index 000000000..63a4cac69 --- /dev/null +++ b/hutool-poi/src/test/java/cn/hutool/poi/excel/test/CellUtilTest.java @@ -0,0 +1,19 @@ +package cn.hutool.poi.excel.test; + +import org.apache.poi.ss.usermodel.BuiltinFormats; +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.lang.Console; + +public class CellUtilTest { + + @Test + @Ignore + public void isDateTest() { + String[] all = BuiltinFormats.getAll(); + for(int i = 0 ; i < all.length; i++) { + Console.log("{} {}", i, all[i]); + } + } +} diff --git a/hutool-poi/src/test/java/cn/hutool/poi/excel/test/ExcelReadTest.java b/hutool-poi/src/test/java/cn/hutool/poi/excel/test/ExcelReadTest.java new file mode 100644 index 000000000..b909466db --- /dev/null +++ b/hutool-poi/src/test/java/cn/hutool/poi/excel/test/ExcelReadTest.java @@ -0,0 +1,193 @@ +package cn.hutool.poi.excel.test; + +import java.util.List; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.lang.Console; +import cn.hutool.core.map.MapUtil; +import cn.hutool.poi.excel.ExcelReader; +import cn.hutool.poi.excel.ExcelUtil; + +/** + * Excel读取单元测试 + * + * @author Looly + * + */ +public class ExcelReadTest { + + @Test + public void aliasTest() { + ExcelReader reader = ExcelUtil.getReader(ResourceUtil.getStream("alias.xlsx")); + + //读取单个单元格内容测试 + Object value = reader.readCellValue(1, 2); + Assert.assertEquals("仓库", value); + + Map headerAlias = MapUtil.newHashMap(); + headerAlias.put("用户姓名", "userName"); + headerAlias.put("库房", "storageName"); + headerAlias.put("盘点权限", "checkPerm"); + headerAlias.put("领料审批权限", "allotAuditPerm"); + reader.setHeaderAlias(headerAlias); + + // 读取list时默认首个非空行为标题 + List> read = reader.read(); + Assert.assertEquals("userName", read.get(0).get(0)); + Assert.assertEquals("storageName", read.get(0).get(1)); + Assert.assertEquals("checkPerm", read.get(0).get(2)); + Assert.assertEquals("allotAuditPerm", read.get(0).get(3)); + + List> readAll = reader.readAll(); + for (Map map : readAll) { + Assert.assertTrue(map.containsKey("userName")); + Assert.assertTrue(map.containsKey("storageName")); + Assert.assertTrue(map.containsKey("checkPerm")); + Assert.assertTrue(map.containsKey("allotAuditPerm")); + } + } + + @Test + public void excelReadTestOfEmptyLine() { + ExcelReader reader = ExcelUtil.getReader(ResourceUtil.getStream("priceIndex.xls")); + List> readAll = reader.readAll(); + + Assert.assertEquals(4, readAll.size()); + } + + @Test + public void excelReadTest() { + ExcelReader reader = ExcelUtil.getReader(ResourceUtil.getStream("aaa.xlsx")); + List> readAll = reader.read(); + + // 标题 + Assert.assertEquals("姓名", readAll.get(0).get(0)); + Assert.assertEquals("性别", readAll.get(0).get(1)); + Assert.assertEquals("年龄", readAll.get(0).get(2)); + Assert.assertEquals("鞋码", readAll.get(0).get(3)); + + // 第一行 + Assert.assertEquals("张三", readAll.get(1).get(0)); + Assert.assertEquals("男", readAll.get(1).get(1)); + Assert.assertEquals(11L, readAll.get(1).get(2)); + Assert.assertEquals(41.5D, readAll.get(1).get(3)); + } + + @Test + public void excelReadAsTextTest() { + ExcelReader reader = ExcelUtil.getReader(ResourceUtil.getStream("aaa.xlsx")); + Assert.assertNotNull(reader.readAsText(false)); + } + + @Test + public void excel03ReadTest() { + ExcelReader reader = ExcelUtil.getReader(ResourceUtil.getStream("aaa.xls")); + List> readAll = reader.read(); + + // for (List list : readAll) { + // Console.log(list); + // } + + // 标题 + Assert.assertEquals("姓名", readAll.get(0).get(0)); + Assert.assertEquals("性别", readAll.get(0).get(1)); + Assert.assertEquals("年龄", readAll.get(0).get(2)); + Assert.assertEquals("分数", readAll.get(0).get(3)); + + // 第一行 + Assert.assertEquals("张三", readAll.get(1).get(0)); + Assert.assertEquals("男", readAll.get(1).get(1)); + Assert.assertEquals(11L, readAll.get(1).get(2)); + Assert.assertEquals(33.2D, readAll.get(1).get(3)); + } + + @Test + public void excel03ReadTest2() { + ExcelReader reader = ExcelUtil.getReader(ResourceUtil.getStream("aaa.xls"), "校园入学"); + List> readAll = reader.read(); + + // 标题 + Assert.assertEquals("班级", readAll.get(0).get(0)); + Assert.assertEquals("年级", readAll.get(0).get(1)); + Assert.assertEquals("学校", readAll.get(0).get(2)); + Assert.assertEquals("入学时间", readAll.get(0).get(3)); + Assert.assertEquals("更新时间", readAll.get(0).get(4)); + } + + @Test + public void excelReadToMapListTest() { + ExcelReader reader = ExcelUtil.getReader(ResourceUtil.getStream("aaa.xlsx")); + List> readAll = reader.readAll(); + + Assert.assertEquals("张三", readAll.get(0).get("姓名")); + Assert.assertEquals("男", readAll.get(0).get("性别")); + Assert.assertEquals(11L, readAll.get(0).get("年龄")); + } + + @Test + public void excelReadToBeanListTest() { + ExcelReader reader = ExcelUtil.getReader(ResourceUtil.getStream("aaa.xlsx")); + reader.addHeaderAlias("姓名", "name"); + reader.addHeaderAlias("年龄", "age"); + reader.addHeaderAlias("性别", "gender"); + + List all = reader.readAll(Person.class); + Assert.assertEquals("张三", all.get(0).getName()); + Assert.assertEquals("男", all.get(0).getGender()); + Assert.assertEquals(Integer.valueOf(11), all.get(0).getAge()); + } + + @Test + @Ignore + public void excelReadToBeanListTest2() { + ExcelReader reader = ExcelUtil.getReader("f:/test/toBean.xlsx"); + reader.addHeaderAlias("姓名", "name"); + reader.addHeaderAlias("年龄", "age"); + reader.addHeaderAlias("性别", "gender"); + + List all = reader.read(0,2, Person.class); + for (Person person : all) { + Console.log(person); + } + } + + public static class Person { + private String name; + private String gender; + private Integer age; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getGender() { + return gender; + } + + public void setGender(String gender) { + this.gender = gender; + } + + public Integer getAge() { + return age; + } + + public void setAge(Integer age) { + this.age = age; + } + + @Override + public String toString() { + return "Person [name=" + name + ", gender=" + gender + ", age=" + age + "]"; + } + } +} diff --git a/hutool-poi/src/test/java/cn/hutool/poi/excel/test/ExcelSaxReadTest.java b/hutool-poi/src/test/java/cn/hutool/poi/excel/test/ExcelSaxReadTest.java new file mode 100644 index 000000000..dbf98d3b4 --- /dev/null +++ b/hutool-poi/src/test/java/cn/hutool/poi/excel/test/ExcelSaxReadTest.java @@ -0,0 +1,107 @@ +package cn.hutool.poi.excel.test; + +import java.util.List; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Console; +import cn.hutool.core.util.StrUtil; +import cn.hutool.poi.excel.ExcelUtil; +import cn.hutool.poi.excel.sax.Excel03SaxReader; +import cn.hutool.poi.excel.sax.handler.RowHandler; + +/** + * Excel sax方式读取 + * + * @author looly + * + */ +public class ExcelSaxReadTest { + + @Test + @Ignore + public void readBlankLineTest() { + ExcelUtil.readBySax("e:/ExcelBlankLine.xlsx", 0, new RowHandler() { + + @Override + public void handle(int sheetIndex, int rowIndex, List rowList) { + if (StrUtil.isAllEmpty(Convert.toStrArray(rowList))) { + return; + } + Console.log(rowList); + } + }); + } + + @Test + public void readBySaxTest() { + ExcelUtil.readBySax("blankAndDateTest.xlsx", 0, createRowHandler()); + } + + @Test + @Ignore + public void readBySaxTest2() { + ExcelUtil.readBySax("e:/B23_20180404164901240.xlsx", 2, new RowHandler() { + @Override + public void handle(int sheetIndex, int rowIndex, List rowList) { + Console.log(rowList); + } + }); + } + + @Test + @Ignore + public void readBySaxTest3() { + ExcelUtil.readBySax("e:/excel/writeMapTest.xlsx", 0, new RowHandler() { + + @Override + public void handle(int sheetIndex, int rowIndex, List rowList) { + Console.log(rowList); + } + }); + } + + @Test + public void excel07Test() { + // 工具化快速读取 + ExcelUtil.read07BySax("aaa.xlsx", 0, createRowHandler()); + } + + @Test + public void excel03Test() { + Excel03SaxReader reader = new Excel03SaxReader(createRowHandler()); + reader.read("aaa.xls", 1); + // Console.log("Sheet index: [{}], Sheet name: [{}]", reader.getSheetIndex(), reader.getSheetName()); + ExcelUtil.read03BySax("aaa.xls", 1, createRowHandler()); + } + + @Test + @Ignore + public void readBySaxTest4() { + ExcelUtil.readBySax("e:/excel/single_line.xlsx", 2, createRowHandler()); + } + + @Test + @Ignore + public void readBySaxTest5() { + ExcelUtil.readBySax("f:\\test\\222.xlsx", 0, createRowHandler()); + } + + private RowHandler createRowHandler() { + return new RowHandler() { + + @Override + public void handle(int sheetIndex, int rowIndex, List rowlist) { +// Console.log("[{}] [{}] {}", sheetIndex, rowIndex, rowlist); + if (5 != rowIndex && 6 != rowIndex) { + // 测试样例中除第五行、第六行都为非空行 + Assert.assertTrue(CollUtil.isNotEmpty(rowlist)); + } + } + }; + } +} diff --git a/hutool-poi/src/test/java/cn/hutool/poi/excel/test/ExcelUtilTest.java b/hutool-poi/src/test/java/cn/hutool/poi/excel/test/ExcelUtilTest.java new file mode 100644 index 000000000..882f1289d --- /dev/null +++ b/hutool-poi/src/test/java/cn/hutool/poi/excel/test/ExcelUtilTest.java @@ -0,0 +1,39 @@ +package cn.hutool.poi.excel.test; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.poi.excel.ExcelUtil; + +public class ExcelUtilTest { + + @Test + public void indexToColNameTest() { + Assert.assertEquals("A", ExcelUtil.indexToColName(0)); + Assert.assertEquals("B", ExcelUtil.indexToColName(1)); + Assert.assertEquals("C", ExcelUtil.indexToColName(2)); + + Assert.assertEquals("AA", ExcelUtil.indexToColName(26)); + Assert.assertEquals("AB", ExcelUtil.indexToColName(27)); + Assert.assertEquals("AC", ExcelUtil.indexToColName(28)); + + Assert.assertEquals("AAA", ExcelUtil.indexToColName(702)); + Assert.assertEquals("AAB", ExcelUtil.indexToColName(703)); + Assert.assertEquals("AAC", ExcelUtil.indexToColName(704)); + } + + @Test + public void colNameToIndexTest() { + Assert.assertEquals(704, ExcelUtil.colNameToIndex("AAC")); + Assert.assertEquals(703, ExcelUtil.colNameToIndex("AAB")); + Assert.assertEquals(702, ExcelUtil.colNameToIndex("AAA")); + + Assert.assertEquals(28, ExcelUtil.colNameToIndex("AC")); + Assert.assertEquals(27, ExcelUtil.colNameToIndex("AB")); + Assert.assertEquals(26, ExcelUtil.colNameToIndex("AA")); + + Assert.assertEquals(2, ExcelUtil.colNameToIndex("C")); + Assert.assertEquals(1, ExcelUtil.colNameToIndex("B")); + Assert.assertEquals(0, ExcelUtil.colNameToIndex("A")); + } +} diff --git a/hutool-poi/src/test/java/cn/hutool/poi/excel/test/ExcelWriteTest.java b/hutool-poi/src/test/java/cn/hutool/poi/excel/test/ExcelWriteTest.java new file mode 100644 index 000000000..5b4c71001 --- /dev/null +++ b/hutool-poi/src/test/java/cn/hutool/poi/excel/test/ExcelWriteTest.java @@ -0,0 +1,368 @@ +/** + * + */ +package cn.hutool.poi.excel.test; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.FillPatternType; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.IndexedColors; +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.poi.excel.ExcelUtil; +import cn.hutool.poi.excel.ExcelWriter; +import cn.hutool.poi.excel.style.StyleUtil; + +/** + * 写出Excel单元测试 + * + * @author looly + */ +public class ExcelWriteTest { + + @Test + @Ignore + public void writeTest2() { + List row = CollUtil.newArrayList("姓名", "加班日期", "下班时间", "加班时长", "餐补", "车补次数", "车补", "总计"); + ExcelWriter overtimeWriter = ExcelUtil.getWriter("e:/excel/single_line.xlsx"); + overtimeWriter.writeRow(row); + overtimeWriter.close(); + } + + @Test + @Ignore + public void writeWithSheetTest() { + ExcelWriter writer = ExcelUtil.getWriterWithSheet("表格1"); + + // 写出第一张表 + List row = CollUtil.newArrayList("姓名", "加班日期", "下班时间", "加班时长", "餐补", "车补次数", "车补", "总计"); + writer.writeRow(row); + + // 写出第二张表 + writer.setSheet("表格2"); + List row2 = CollUtil.newArrayList("姓名2", "加班日期2", "下班时间2", "加班时长2", "餐补2", "车补次数2", "车补2", "总计2"); + writer.writeRow(row2); + + // 生成文件或导出Excel + writer.flush(FileUtil.file("f:/test/writeWithSheetTest.xlsx")); + + writer.close(); + } + + @Test + @Ignore + public void writeTest() { + List row1 = CollUtil.newArrayList("aaaaa", "bb", "cc", "dd", DateUtil.date(), 3.22676575765); + List row2 = CollUtil.newArrayList("aa1", "bb1", "cc1", "dd1", DateUtil.date(), 250.7676); + List row3 = CollUtil.newArrayList("aa2", "bb2", "cc2", "dd2", DateUtil.date(), 0.111); + List row4 = CollUtil.newArrayList("aa3", "bb3", "cc3", "dd3", DateUtil.date(), 35); + List row5 = CollUtil.newArrayList("aa4", "bb4", "cc4", "dd4", DateUtil.date(), 28.00); + + List> rows = CollUtil.newArrayList(row1, row2, row3, row4, row5); + for (int i = 0; i < 400; i++) { + // 超大列表写出测试 + rows.add(ObjectUtil.clone(row1)); + } + + String filePath = "e:/writeTest.xlsx"; + FileUtil.del(filePath); + // 通过工具类创建writer + ExcelWriter writer = ExcelUtil.getWriter(filePath); + // 通过构造方法创建writer + // ExcelWriter writer = new ExcelWriter("d:/writeTest.xls"); + + // 跳过当前行,既第一行,非必须,在此演示用 + writer.passCurrentRow(); + // 合并单元格后的标题行,使用默认标题样式 + writer.merge(row1.size() - 1, "测试标题"); + // 一次性写出内容,使用默认样式 + writer.write(rows); + writer.autoSizeColumn(0, true); + // 关闭writer,释放内存 + writer.close(); + } + + @Test + @Ignore + public void mergeTest() { + List row1 = CollUtil.newArrayList("aa", "bb", "cc", "dd", DateUtil.date(), 3.22676575765); + List row2 = CollUtil.newArrayList("aa1", "bb1", "cc1", "dd1", DateUtil.date(), 250.7676); + List row3 = CollUtil.newArrayList("aa2", "bb2", "cc2", "dd2", DateUtil.date(), 0.111); + List row4 = CollUtil.newArrayList("aa3", "bb3", "cc3", "dd3", DateUtil.date(), 35); + List row5 = CollUtil.newArrayList("aa4", "bb4", "cc4", "dd4", DateUtil.date(), 28.00); + + List> rows = CollUtil.newArrayList(row1, row2, row3, row4, row5); + + // 通过工具类创建writer + ExcelWriter writer = ExcelUtil.getWriter("e:/mergeTest.xlsx"); + CellStyle style = writer.getStyleSet().getHeadCellStyle(); + StyleUtil.setColor(style, IndexedColors.RED, FillPatternType.SOLID_FOREGROUND); + + // 跳过当前行,既第一行,非必须,在此演示用 + writer.passCurrentRow(); + // 合并单元格后的标题行,使用默认标题样式 + writer.merge(row1.size() - 1, "测试标题"); + // 一次性写出内容,使用默认样式 + writer.write(rows); + + // 合并单元格后的标题行,使用默认标题样式 + writer.merge(7, 10, 4, 10, "测试Merge", false); + + // 关闭writer,释放内存 + writer.close(); + } + + @Test + @Ignore + public void mergeTest2() { + Map row1 = new LinkedHashMap<>(); + row1.put("姓名", "张三"); + row1.put("年龄", 23); + row1.put("成绩", 88.32); + row1.put("是否合格", true); + row1.put("考试日期", DateUtil.date()); + + Map row2 = new LinkedHashMap<>(); + row2.put("姓名", "李四"); + row2.put("年龄", 33); + row2.put("成绩", 59.50); + row2.put("是否合格", false); + row2.put("考试日期", DateUtil.date()); + + ArrayList> rows = CollUtil.newArrayList(row1, row2); + + // 通过工具类创建writer + ExcelWriter writer = ExcelUtil.getWriter("e:/writeMapTest.xlsx"); + // 合并单元格后的标题行,使用默认标题样式 + try { + writer.merge(row1.size() - 1, "一班成绩单"); + } catch (Exception e) { + e.printStackTrace(); + } + + // 一次性写出内容,使用默认样式,强制输出标题 + writer.write(rows, true); + // 关闭writer,释放内存 + writer.close(); + } + + @Test + @Ignore + public void writeMapTest() { + Map row1 = new LinkedHashMap<>(); + row1.put("姓名", "张三"); + row1.put("年龄", 23); + row1.put("成绩", 88.32); + row1.put("是否合格", true); + row1.put("考试日期", DateUtil.date()); + + Map row2 = new LinkedHashMap<>(); + row2.put("姓名", "李四"); + row2.put("年龄", 33); + row2.put("成绩", 59.50); + row2.put("是否合格", false); + row2.put("考试日期", DateUtil.date()); + + ArrayList> rows = CollUtil.newArrayList(row1, row2); + + // 通过工具类创建writer + ExcelWriter writer = ExcelUtil.getWriter("e:/excel/writeMapTest.xlsx"); + + // 设置内容字体 + Font font = writer.createFont(); + font.setBold(true); + font.setColor(Font.COLOR_RED); + font.setItalic(true); + writer.getStyleSet().setFont(font, true); + + // 合并单元格后的标题行,使用默认标题样式 + writer.merge(row1.size() - 1, "一班成绩单"); + // 一次性写出内容,使用默认样式 + writer.write(rows, true); + // 关闭writer,释放内存 + writer.close(); + } + + @Test + @Ignore + public void writeMapTest2() { + Map row1 = MapUtil.newHashMap(true); + row1.put("姓名", "张三"); + row1.put("年龄", 23); + row1.put("成绩", 88.32); + row1.put("是否合格", true); + row1.put("考试日期", DateUtil.date()); + + // 通过工具类创建writer + ExcelWriter writer = ExcelUtil.getWriter("e:/writeMapTest2.xlsx"); + + // 一次性写出内容,使用默认样式 + writer.writeRow(row1, true); + // 关闭writer,释放内存 + writer.close(); + } + + @Test + @Ignore + public void writeMapAliasTest() { + Map row1 = new LinkedHashMap<>(); + row1.put("name", "张三"); + row1.put("age", 22); + row1.put("isPass", true); + row1.put("score", 66.30); + row1.put("examDate", DateUtil.date()); + Map row2 = new LinkedHashMap<>(); + row2.put("name", "李四"); + row2.put("age", 233); + row2.put("isPass", false); + row2.put("score", 32.30); + row2.put("examDate", DateUtil.date()); + + List> rows = CollUtil.newArrayList(row1, row2); + // 通过工具类创建writer + String file = "e:/writeMapAlias.xlsx"; + FileUtil.del(file); + ExcelWriter writer = ExcelUtil.getWriter(file); + // 自定义标题 + writer.addHeaderAlias("name", "姓名"); + writer.addHeaderAlias("age", "年龄"); + writer.addHeaderAlias("score", "分数"); + writer.addHeaderAlias("isPass", "是否通过"); + writer.addHeaderAlias("examDate", "考试时间"); + // 合并单元格后的标题行,使用默认标题样式 + writer.merge(4, "一班成绩单"); + // 一次性写出内容,使用默认样式 + writer.write(rows, true); + // 关闭writer,释放内存 + writer.close(); + } + + @Test + @Ignore + public void writeMapOnlyAliasTest() { + Map row1 = new LinkedHashMap<>(); + row1.put("name", "张三"); + row1.put("age", 22); + row1.put("isPass", true); + row1.put("score", 66.30); + row1.put("examDate", DateUtil.date()); + Map row2 = new LinkedHashMap<>(); + row2.put("name", "李四"); + row2.put("age", 233); + row2.put("isPass", false); + row2.put("score", 32.30); + row2.put("examDate", DateUtil.date()); + + List> rows = CollUtil.newArrayList(row1, row2); + // 通过工具类创建writer + String file = "f:/test/test_alias.xlsx"; + FileUtil.del(file); + ExcelWriter writer = ExcelUtil.getWriter(file); + writer.setOnlyAlias(true); + // 自定义标题 + writer.addHeaderAlias("name", "姓名"); + writer.addHeaderAlias("age", "年龄"); + // 合并单元格后的标题行,使用默认标题样式 + writer.merge(4, "一班成绩单"); + // 一次性写出内容,使用默认样式 + writer.write(rows, true); + // 关闭writer,释放内存 + writer.close(); + } + + @Test +// @Ignore + public void writeMapOnlyAliasTest2() { + Map row1 = new LinkedHashMap<>(); + row1.put("name", "张三"); + row1.put("age", 22); + row1.put("isPass", true); + row1.put("score", 66.30); + row1.put("examDate", DateUtil.date()); + Map row2 = new LinkedHashMap<>(); + row2.put("name", "李四"); + row2.put("age", 233); + row2.put("isPass", false); + row2.put("score", 32.30); + row2.put("examDate", DateUtil.date()); + + List> rows = CollUtil.newArrayList(row1, row2); + // 通过工具类创建writer + String file = "f:/test/test_alias.xls"; + ExcelWriter writer = ExcelUtil.getWriter(file, "test1"); +// writer.setOnlyAlias(true); + // 自定义标题 + writer.addHeaderAlias("name", "姓名"); + writer.addHeaderAlias("age", "年龄"); + // 一次性写出内容,使用默认样式 + writer.write(rows, true); + // 关闭writer,释放内存 + writer.close(); + } + + @Test + @Ignore + public void writeBeanTest() { + TestBean bean1 = new TestBean(); + bean1.setName("张三"); + bean1.setAge(22); + bean1.setPass(true); + bean1.setScore(66.30); + bean1.setExamDate(DateUtil.date()); + + TestBean bean2 = new TestBean(); + bean2.setName("李四"); + bean2.setAge(28); + bean2.setPass(false); + bean2.setScore(38.50); + bean2.setExamDate(DateUtil.date()); + + List rows = CollUtil.newArrayList(bean1, bean2); + // 通过工具类创建writer + String file = "e:/writeBeanTest.xlsx"; + FileUtil.del(file); + ExcelWriter writer = ExcelUtil.getWriter(file); + // 自定义标题 + writer.addHeaderAlias("name", "姓名"); + writer.addHeaderAlias("age", "年龄"); + writer.addHeaderAlias("score", "分数"); + writer.addHeaderAlias("isPass", "是否通过"); + writer.addHeaderAlias("examDate", "考试时间"); + // 合并单元格后的标题行,使用默认标题样式 + writer.merge(4, "一班成绩单"); + // 一次性写出内容,使用默认样式 + writer.write(rows, true); + // 关闭writer,释放内存 + writer.close(); + } + + @Test + @Ignore + public void writeCellValueTest() { + ExcelWriter writer = new ExcelWriter("d:/cellValueTest.xls"); + writer.writeCellValue(3, 5, "aaa"); + writer.writeCellValue(3, 5, "aaa"); + writer.close(); + } + + @Test + @Ignore + public void addSelectTest() { + List row = CollUtil.newArrayList("姓名", "加班日期", "下班时间", "加班时长", "餐补", "车补次数", "车补", "总计"); + ExcelWriter overtimeWriter = ExcelUtil.getWriter("f:/excel/single_line.xlsx"); + overtimeWriter.writeCellValue(3, 4, "AAAA"); + overtimeWriter.addSelect(3, 4, row.toArray(new String[row.size()])); + overtimeWriter.close(); + } +} diff --git a/hutool-poi/src/test/java/cn/hutool/poi/excel/test/TestBean.java b/hutool-poi/src/test/java/cn/hutool/poi/excel/test/TestBean.java new file mode 100644 index 000000000..af68870bd --- /dev/null +++ b/hutool-poi/src/test/java/cn/hutool/poi/excel/test/TestBean.java @@ -0,0 +1,51 @@ +package cn.hutool.poi.excel.test; + +import java.util.Date; + +public class TestBean { + private String name; + private int age; + private double score; + private boolean isPass; + private Date examDate; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public double getScore() { + return score; + } + + public void setScore(double score) { + this.score = score; + } + + public boolean isPass() { + return isPass; + } + + public void setPass(boolean isPass) { + this.isPass = isPass; + } + + public Date getExamDate() { + return examDate; + } + + public void setExamDate(Date examDate) { + this.examDate = examDate; + } +} diff --git a/hutool-poi/src/test/java/cn/hutool/poi/word/test/WordWriterTest.java b/hutool-poi/src/test/java/cn/hutool/poi/word/test/WordWriterTest.java new file mode 100644 index 000000000..8689e0b56 --- /dev/null +++ b/hutool-poi/src/test/java/cn/hutool/poi/word/test/WordWriterTest.java @@ -0,0 +1,24 @@ +package cn.hutool.poi.word.test; + +import java.awt.Font; + +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.lang.Console; +import cn.hutool.poi.word.Word07Writer; + +public class WordWriterTest { + + @Test + @Ignore + public void writeTest() { + Word07Writer writer = new Word07Writer(); + writer.addText(new Font("方正小标宋简体", Font.PLAIN, 22), "我是第一部分", "我是第二部分"); + writer.addText(new Font("宋体", Font.PLAIN, 22), "我是正文第一部分", "我是正文第二部分"); + writer.flush(FileUtil.file("e:/wordWrite.docx")); + writer.close(); + Console.log("OK"); + } +} diff --git a/hutool-poi/src/test/resources/aaa.xls b/hutool-poi/src/test/resources/aaa.xls new file mode 100644 index 0000000000000000000000000000000000000000..cbb53e161fea6e33aa215f0efc8de8aceccae6e6 GIT binary patch literal 25088 zcmeHP3y@URnf~wXH#jr{2qVhS%m4%P5@rxEJi5`C#4IR+8YvJ+pc^c4Ng=w~($UfA zZW&0T17ZSfjBALSO*RoVA*q;%RhHRJjhpO>g&N8=yJ)%V3MFe^mfHJ$=k&dO@9lG& zZe#5u>C~`%`ZQ2aN;i|5Ee^B-Ycdg?txo)KG#W)7UUO; zOgi^oJOk7S|Bp0~N}?g-G3mVcnZ!ScE%BttFL8f3`XsI*|BARqKF#OM|75s8$?!>w zG8~5CEwbWb0Tz+{xHijKlt{*?>sWRDjw<`Ox-LQv!($6uCe8<*AYNtXRJqTqYn!@` z!qt*P3g<-ynJwew7|JK*ZlcLwe8aYOtX_HP2MK=+bc?svPJc5bnQxG%+p++EoQm+t z84ZCSLtRbfS>JI6`h+(d;TH5$$(Qu!guqiqxGfWm^hHL5z~_X(7aHM~WQ}}978}cp zqyq5PXp6*TtVr8|w)XD!_E`(?u%<^nwnitxKel-CRMs$XqKeIvPU#a_mjU(8jAFkov!IkL$x{Zj^|Yx(Ze6A|+AA~h1F9FVGvh_- z%=j>}(k&nIGlLLi@+DLs3-GSj)zC9^$Dow6?9G9Z9C5jJrqoX%8T$(ak*%qTst znb{hh41G`Z<|<|Rie#Yiv)v-(!>4CTw8^sBBfDYXzL-c;X{$2Rrg~0zd1pe(1_`VSMcS7Lb3W5Js2z*Bf zJWQT2`j3Xt`AP`9UU|M2Lg!!z{IL=|rTfKib#r0-R`3Lps=vJDhhxH_;YVaS9w$5H zQ-1hJ2R@<<&)_j#0T0jzqNX3PlQSyd+D@7r_!0Re#tB!RIGm!AKTxkYJhKTt%AvEn zx%s}4_l=b39;F8etjq5%6rPy;#AJycQgC__lYW{*;GE&8@<(Jn9^n+7{?cWXCyqhD zBqzo;4R`hPO(oAb2M+nQoLYYPW?kN`m+lwi9k@&9$BIrR{Bt4jAB4cQ9ahrOc327j zuA<}m=l#vi<42B%{`V?4+gJISkKutKzF5IUMrn9dgQetu1f!8EUjZLi0lzspPHv__ zyWs-ac%ZF!AlV}WJ~#{5(u8p&f+tMyL=89?q(@=CVOJusNL>h=7S@G8LFz)FLh3?@ z)r-J3tcydUUIaEuT^#6^>O!EustbV*t1bk3vbqr11?obeFRKfIPOd7#l0bi;_lt!0 zhp6f(`2tN2vtG<=Gns*8kO*C64Mb>$;Y8?HYal|?3@1XTS_2W9Yd8^l;2Ma~gu{u@ zUDrT_W*tt1KK8UkhL=`voe8xOsopwRZPg-4_13|PtQI2GTL%MfEkvrf4%R8P5UJie z=hQ-^ufKZhOsb7Y_13{c_Vm^1>#yEAm~quYqk-iN?s=C4; zLc0%vqq28uU{$)ksXj4-2zIhG$*A7`(`q47%vL8dy%r)tOAOY(14POStEf(8{b#sb zIp^6g@uF8@HpdQT6o2xM#c&rl(?0J%PkM=55g5ffK_nVJiX{v!;cXoZYARID z{<=W8tdkn1i`}O~xF;Zb4-gVcbn5Eufsp#2I(5obBCEr~x88bd(Gi{13Bpny7>cVf z!T=jc28H4q1uYcKfWg?Z2M!#l5GtF^RtV*VIYJSpNpsUt3-5(hwgq2-=(cbw>JzFG zVU`a8-2A3hY0%g>D|tX;TR1u{4q9%tE4gB~us)ONKOI7Fx;4iq=KfCv>gD#k*Y|tG z3^wt3AX2tFYTlZUJdK+!S+TjCST7fv1fg_+M7GoO9~pXo+E^wuRl@qFR+ zH(xUI8EfV<$H1r4$VWSQrME=BaQt8X)68d_na_C!KD|ah90yAEN#+YLp7<{_pEJ#T z<{J3)8Trt)m-wXeg?In`1v8)Vh}+s`Lvk%nCz{?$j57M!%Lq({TVMad zul~f$Cu8PgvbR}AK3;q4>mPdNn3>NMGar+^%`x)v+FM`$+fRJa#3w3kgR$aJ@*KEL z_`_1LVzFXGZn-hO;>fiG?XpwSj-lU5Z2r=hhLT6BPL%wpvF>b zm)#^&F&9<1dYoPqz#m+GT>C$4sSdW>sH>}pUdK6C4%G?Rha1_dn~Xf#XEX!F-Tb(hQ9KLJYM zB_UW3tg2)^5v&JRd0BHW#9_@1lM?F`UWmeIKDuSW`ij9aau)bAhwBujOfUbqr@<}E z{L9TBhiaqJH5f&eCi-;}i-Kgw=dHEW5leLsCQvF9D3ySyEVUXw77A2lQ7g`;s_?*}PZlnN<4S{+MW-BDbjp!Mrzp$0l;z*< z`sqC#>;E#}y=f@P$+gmyx>)vk;8)IfEXb*Bsr8$nUv&Gzj*nlisRTNJJrW(j$@S8J zoQ?&h;SnIqd%F*8aP*Q5$kGB?%K47_pe$~?(0&NuHi@>MzQG7Aqt#C;7J)z)u3UC$ ze8uiPtSN1%e=v@cQv)UA#e`eZZ%1=zNA}%4mkcH6`H`%1U`NdH_M9TNL>xQfzKPp& z&Lf>&Uwr$rN_#vLbeQ9{0k1u>AuDX4Y>&+5u|2I5o$}g%g?gtjuTcE+2L9s||GbYs zH$a?vWV1K=`Pbhcnq(err@XXV;MZkz5}L}%b@W*tA+Vf7^SOP2?tOll9tVsdy(um| z@3aOdDV8r&^{SwEewiMJiy*yeE3OFkB7gF&r_J;hmg#W{5~MfVrRSY?i2P>HM@;lmvWWKe+LoW*Gn8Bl zddjh=i)D|1eKAMa^-KPiyCsf$*)D&so|YlCQLd32`CpuFnH+Lm2 zD>vu!+h^a2eTsM3^b)nEY<}a){AGpJEVY~4ZceHGy~!5Y>ybU~uk~Yg?1+-bt zDB;9cd^#T#rii0nNqGwIYk2Ns4_=YHg!?UcEAcdJV?%O0qN6O4@p$Bpuq}IFS$jM= z7rbK0KEU>QV6?=Yfb9&xc!ouPDKOgirNBtrZ5LQKPBFuc-U0h>VmF6;HhNWBJvZYj z=p9LU=NoVS^RiDZ&#qH&P89`bg?YVQK?YIA3DYnXi?mp`A?L{`dC}%St4iOYuG3Mv zpu(%!4$t!pm&$9|nV#omN?Q*7_j|XZkvLDsUwCp5JBTUSJ918PBWj$+R|c3qH101Xth zc?O6+jDf9^wArKh+@eLZ(h9~z5Jq`}Fm5^rVSMf2g}sZ_FawVhiRcuyOddLAEBkU% z5u}<+c`TF0vQpvQR25d%w((%aJbSO~Zj`T()Ie)%-{5 zixZ7IHwQVk9n3|PB-~O=tN91B-K${vJI5 z{~W=u_Y8Vy5vHT~U6j(c?(}$VKeTA4$HxpkJ_sYlAdE&Dgt12kVVre)VY;XDn;puc zQx;NuA*WL|QnYBDiXg?YQKzCv@xUYdp7sfHcrE*$jLuoAjP?dRcBy?s1YZpezH+?e z8?fS#rh6xSz4y?|S>69tZw%1D8V^0up~3IevLbJ48aA}LM4rFB2Y)9O4Qc`s1ZYHj zG}ty-`RZ?v=g5_Ms>F!;Fro^Bk`sek1V-k%9P64a)it3o=*wBxgu*xs9`u6=CFjW# zDrAqsW1WD)Hp)lj!}#^#r~Hx{0&<|haN>l(Ck6e|f1FT$Nt+H=oqYN|{gNJS6jNNj zj(5=icV`*wv7EQ)F_^7n6`h{y+_7EvKF4532j~$M;{@|6+r!r5ghpg1V5qr0bn(XZ z4^ac}Vg9J+JO0|yNM644{!_oZbo1zM-jkHBjz?dZ1sHbwm1kjo4v~K8V~FhFmm_jL zx*n1K_cMs}ymuguMZ6o4x^ms}T8d??y!KfNnwL8h$$> z*Znsm@;xN?0k|Kq6OsD^hY(Hs0eVe8TG2w0(0ePGuq<~v_4{`v+Rq<6^6=lk{!b5; z;r((3ijoz#8d&@XUpxAngKsG8#ts7X(FhF-67pbhoqE9_s}g2-3W|;G)Q-Yq?EJ>0+J3XDJdu=^^QLG z{(K%D?{D3Ay?@?&);cq5);arg=Irm@pIu*dWh7)E02P1^008a+%1-qPd=LNtASwVr z2tY?PlyP!&w{&zj*7k9>bTi`cb^z1nAtN&701)B#|M&P0*1$XMA;&jdc{5r4RzX5-mXA{G9b+k8&nJ^%0n}UQ&P|J}S zXR%`0Udk@>dbr<2#a&2DNcn9__HVA!MR75r1~!xds;?;I#>S2XjV>6YvBkRIS6}K+I(pp#_fvyWZu(;=TXRF54(BMX(!w-Z*GtP z>VNUrS2|pD-{5Ab0(VF(xW^j1T7um;IevWp*Hiz4-SbbcUXh^su!jpf{6OI{V(@fo z0g5NB>?Nhpc30a!P;vgwtJr)H#X=_|C7w2U2$Fn2yZ_}|*n(*6_Tb%gNM&~-jcCx#%zCcM zZxDR<_?%ov%SOny#w^=a_@0lk1-R?$gN#nXD?fI%%t6h2d<17a@8t*RbB+TH8u$)} zvwV65FnY=dzYGP((3C9x)=3sIu9y(vS0<4F0CE62f;X7+4?pp8aYg=$@SzlwI*J|( zWiHr#)4{F4DUpa&>DCIn=_kQH21MZz9O3?$*s;e6SF7nKln*gD_sy9Hygo)`E|?u+ z{f=hUb^}m<*j5YNg6_ag18zZoww=txAx9V&UibmZcU`FSsWVfifqYbqNrCu{NmhnqH1ALI zQRu~;wOlQ6fZvd_7c0aA45%w$4iyXd$fU+&!l4Y-?_$XLPR(I~-3pdj8wjdRmK`o# z%Em~@m3qk_x1tul2FA60F>FH}&8{q>N>sw9kw|?=Z%qKYlsq}SN!@#$&v2SDaDE(NrZ)ays-dw?UDv&+t|b_ zD!Xkfv(|0VzDKi0g*?!|dh&+r<0&!)7?FwLO+T(`Ge)Mi4`ua08&bVUc^Y@Glg1Hg zspeF}TjjzsPDsf|Ii-R}cHga)(pgjsFT=9a^kE|2lsUQEIFic?SEH7r4x`yC+ppR% zWY8Wcy!~LH-tia{Pf=#E&{iAE@s(i;F(Qaatu6I1X)+k$dH1XJ)1|iP6m1oP;6U*g z%FV>RN|wsJygZ>Wy(b%FiYxl@G03dVz;PAbWtruSnzT7hTYrXbJ{Vo;8@B81MuOjnr}H@DmKbeQ}uT3t7?a90I!U^9mVII)dI z5?dMNj-Awy9}4fVuJsp9wcbhKM?|Axzjb#ZDX)O(L7;{;sA0a0?TJ3sp#Y$W!DYm% zew;1-j0}J)qgOgrI9NYnz%&yfibZ?jlHirE&93v*&8XEO!bs;>|<`_qfrR*XP3r} zFiwD9Zc@(xQ)IY?yDh$0cA0H$^0F9*0booroJjRKxkW*Q4EhGDnsD$ZfTiF%fn$=D za^(c&<@6HK*V4ScRr+8o_mf9VjE=UdP;8VDZT4n_$03MoBVU~sMFXPYr>pzT-1yZ& zJhiA`>rK}BI$qTZ9}7Xp_4ewLL}!G*@AtkP`Ip}h>mqN~VG*4E-+S+bUXoYetXn_y zhn%dfuf{)tTn!=_<2P%CHrYypK7t}_A{cA<%}I6LCh)EEZ3a?>fr6-u)YGa&B3&a0 zR^pGdn)}<>a~jS9XD)o>s@iI|Sht`1@v2cSqi$A=hRpy3UR4B#!LIm@ut=`%Iijs1 zz1&4rqnhErAe+=ipHqYhn0+c~8F4OiexR8L_3uB$ID{_bPS)7?S~m$P6Y$c%Xy6Yl|P|J;~*eFL4 zqt_=+?}c`c?NhpDP%+&bK|9Z=B=K~1gdKLlD~52p4UY>{3J`}TQ|rNMOPmnrJ>7GE zx_-rtsk9;&{EMLZ2)sU<3|1PykT$Zs>h3#&S=;-tB*A_07QFeq=lK(YlHU?LzMLHi zuQ-b1>fIqUexZ*arKP8v6Mm7GzZjo6_JlEXMP{BJdEt9kzEqZV+{l2^Qeu|>o?s{5Cke^` z8c0NvRBJSXzAzzsxslK8k&3h>cO+kXQ=tz86c2F^4!75D(2qfRKS-KNymI)MDumdN z@Sz`AdGVGVD!5ORxCNPl9=E%T3D^!e209Mfx)P*!RIZu=DZMLJOP{!lHd=}-9;%eH z*$;frv~3PB^{S7)_{<-KV_D~J#3w*9qWa~flVj2Pk*u=Y_{8G|zK)cd&VoWRFYdAB zXN;m&i-{^!r{9>&+w8gqTlolSFgW=ai0Aq8nRw$VV{ht6#kY?G0ca!`U>oT2woKWl z1yXA+N|uZjOOV_2^9N)pT)aF@BYZC?R~=CEfpXGT{rXcMIPE2(rce()!rc0pHzQ-m zS$GmBb9017mK)w|RK3oqshhGZo0~6u5o=du?wrhTP{)5b_C3F_d*u23l~}TFUw%Cw zYO7HLuhe`g--Rj9CUNps*%)zMk|EMX3^$t39uL2lfHV1?y@(3i@|ZOlBOE65(}p&d z%yon`d=M;Us^XEIAM-uSz=#XKQs zrqF+>n=QB}P91~dgpr{hW2RPs|I*1YQ=r?h{!!lMJTi59_*iw6-uH)x+4uLGasU+r8{h zRaTSD@q$ZHfJn-?D;L_>=(9$=Df&5Do*UKrz&yE6kh4ixHX;qYQbZf;&ZYavFrT_& zq`XakelEpK^ExwG6MOY$PV<{TV*XxB)rlm!d?F%U7W1n>EF!lOX?>NRD%3l%LoAuRCX*sLMe9#*BJlb=IeMZv{)(Z*RfA(~Bg&^yF)0S0gsdJ-NrO#^hn)JI zQ(cZLwB*VxR4k7*&C^s)R$vcpJXPxj)~k``v$UvCvDUd7a@1xXr@U1GdZ{Nh}Fl-qufqTt5n@DEgKcd1?pr^nq-+D^RuJv{2_v`!_7ow#U zR=CgECR-81*33z4YQFe>Wbc%!72KW&Vz7%sO+RpSq$8=RDMbxH^@y-)!!$rwq&@z< zbG308rBLyWImIX+uA%c7im{3^JBK4_DyKHW8FMA*Ea9$dT{%FBhx>hK8V=J3kXeJJhDgm#@Q63SB0rtbQ{opa)a5GMd4 zUGO1_lXOiQ-jn-diV|{I;n$N}!ek{okKP!f)on9+RHlN4TDA)K{T>d^8j^z!C7lU^SeDz?@-;5HNl^RlciY3*nS$eUCsBsuhT&0c{n8nBK zV~Bmo&+xzO)hd1=6YoJvsK0A2{z8Jp?e3JM@YD^1iZCHAzc9x#mfcYYbfP}!zE6g8&tu* z;55CIs!o?|vy`hdGqRX+M0lbhm9Toa21a+g8^Y!mVJH`qkzmZa`be=p6?!wu=0@aD zo$n$M2F-2&za4%IW|H)%Idiw;zJUc#9COmCq7q7JP#y30G~n!g$sw|>91l&Ey%(BQ zcqHH*vf-@qz~fQXa*R8xgQ$J(zK5~%#z~Z7|0J%BIX}m!prIN1ER`<~6`~}0BDZC( z9QMFyknbH+hqM>O^X>lkHr+{I? zuoe4k@;D29Gz8o)YlUZ-y@W~kYMoJfz1q|SW`|<0ErP_KO-qeR#(X_yPr{I{F|+*a z8!Rh>nJ*5HLTt;MGB=Yx;1Av;6S>M|hLqTz_NCYJ1ZAM?iW zw}ZZR=(bAhyBii2ZW8Jz9>QH-+UK><=)N?ajvhyG^fs}c3D0x8;*9j0p`|J^NX)ml z)AeB#FuO_a+nijHY{m`@Vm zJuS)VC36{d;gr)Ro^ZGcG!_0-CRBBo>4@00&eet3XqYK#`zcFE36X|D19vG=W?w#C zyxwK>NoDecFr&yB2c+1+1gU}UFpf7M;T^A?yu0G2WA_}Y`2*hLr)5A|`8BdVqZra+ zwYR&Eeai#j#PauLAFI*t?S)g%Ieb|1FP8l$?Pls2Iep{;LS8o9;GMUh-y0)%uI-R- z3rXdxge}Id;+043&7r<6x;iz2;UU@(umnQ|2R1W(A=T~80XuCbGB2!xNtiIqf$C}x zM$XD4B{~9HTV#UxQ)ik9Gl^~F@3(SF`0Ihgo=&kBG5}DkuqMgrzA{Y|6pBsfLX&zj zAC55Z zS6$Ym(Lov_ftJ0!H8_wwBi$T3Orq;y+b(wW2O)3Hu5(U?h`#9(O*VAzjQSD6-X~QW zAk~qD+U|g=y@;leF@d2yleNMVm=upWl4enBW|uXU!G?@OHzM_q@nRq~%ZS~1fWmM+ ze#_U9{3g&{{P>i}HQ=Fpl)^*&7xEzUw6#Hkpu=u4lPWK#Zj@ja()ov@^S;Ohrji!c z`(kF>W;AK8pG!3bGU->`Q$0Qr7gT%MccY}ozNXaAn$MS?kHf3yZK8JB1Q%}FW_vdc ztPoa1LwDK@oP#zR9oz3qK)b}nEsjp!!Kf3k6LV_8vX+U?7E``QBg;FF8l90z<#Ns_ zx?>$!nMmssYb0C`L1)*LwXX@Pw@4};J=@92kF`f0OgYLTh~W2<{&>%Kji0$b@*(@rVBIK*7ih71u@Oi&mPCNI*{mIb#-x4q!l zaGCN;c%$R*mcL6-$_|5*9WEXKfb&Ptb#wOtTe|&Vxdz?gI2b>0fn-nuect|CO+hX8 zu#^}z0)i%Omr9_cZB$*9dV%CPVT-KSf@NC#R@Dypy8sUK{Z=7$a%P&ls6U%k?`F&9 zqB6Cynz61BFLfka=`y}+71#nVJ2L4=Cv#47H_uJF42|jY3_B8ePq7g!6r4_YoD|+{L>m3inhUk9r(blfA#)tXqVzw}OOO_n zOi|bVN+&PyMmZPwxPFeW(3@V+>id|xNVtg9Q%TEv1=BeM3wN)w)A`2@1Z^NAg+;Do zt~vAE;x>@g`E!VU-l^tWG-MwMF%mxKmEFh7qV1Qxp51=A)hVxmOF}NwGM6Fc5PXV( z@NX78m3%=H^r5vP?24UM?ejrgI%(YkKAGhLIjQQ>pJ}?PFf`}*DJ<=U4GzO?a8B?% z-GNmamLf->)2L{)GQg)6fm_~UVWQOmlG)wU zrVy6oHLH}XN+yL&o@v-=VvHT z2M>WG82PV;sjmT$Icj_PlI8~=i8mg%{}C?WNZ!;9G?Eyd4K51?40D4j5iwXmS?tU3?ey8OR3IK;8>Z zkkiy-DWMgQwH`EV&N~A*nhNpyEnRjEj!HeRhiK(`;C-EX)SDL6TSzR&y|gg*Sbq~+ zQKrldjbXz)3g{m9d&GWGIu?P@E)Z{6e=;l1+1oLlFb0}ObV2Z{FglG}do7Pj z);@2HSWx*f&e}zzPoLfhkK@W@ z`6;eH@rtzKp=WO^j$S|Zq&vr_@dwen_}GA_rB(Xdf=8@{hP!Ny$(uN57xp(U ziG~SSbXj(&fV=2XMsMp$%m^WYk*Jz-#2iEwa@ee+iB-88wsLiJg-5j-3bylZ#yWC6 zFJ|I#1lM9jJC~g$o#~S{P5CTBR?GTs?z6Y=pG{otlcMWI&)n6^Cz?n)svI9@G&c2w zjG3Ukj1Jyv%%REe&VPsUvdFCbMc93^VWm0bU13QMzuygnT)a>%=cb4h!{ zn%#D#u@zv2&Nx#<=+rA5MH(2C>n)A3M8M6-;bt*saH9C+cju47u_MYYyA(`<4!hW> zxCCk#Ua0H^UQ=?imsnq1G3R-)n~_EvQM=BQoIrbpeVv6t?!r2qjGr!IZT2#9=AVah zKQ*71X~hx=D8$QlbT$CO3}l%;gI8nwgPRMFVZF?wlL`XT+}l+`}nC zV&RIaUpDX12K=@*s>v=(WPz{TJp5$*gCv~c?BH(cs%7c!{(~ksZ~f-xFtc?uGs(f` zs^j9FM?RB7&>q~$|gWD($!v}+Sl1|MkL z@n*4M!46GkPPduWkZHSvhR!7k_WFCHb`@-`W-c zmpyu?T!oZ@9ILvK+XRBbfeG46bm;d4qTw%g!U(9@;lGf37ooScG`) zjs86>9SE66K;*$hn8iCScwPMP{W<~B0(R)cdEF|M~r!ZF}mf7My|C!T)+^#ciD16ZKz6Wbj7ct@-+G;oCFVU&0{x_#Q5Ndm?)q z;CAo$7eFPPS^n|#|6eEhHsI|}>@UC?_>lNt0{-gD-Uhtg*!%@p2T!*C3izwdc^l<+ zYw8yYKgn-Vem1IZqulOv{6b+T`z^}P*Sd{zyK4M}!btfO#fzW4v$>aXlOwO3W0T6>?p*J&*^BxE1}6@U%^0O$ZR!r_xH2mk;w3IIR^Ku0u@ zb9M0oxp-OV`MZHU&A9xWA+#mPh^$2bMELdpUH*f6pi*zxrI!c4yIE7J8k_s^d2T{$fODLkpx-QBD0a~gRc;-OCwE2ORSek zRUQYD*UE(o;boNuNe!t9f<`&OHe-+-|u&y!l8EAuRg4Opl3rty+S zWou*xSY%e`KrSXn^T4qZ;gvHV4K{x?27Vnc@Q~I%6qTD{xoxDC53_I4FVJ#ZiLC9} z*Xm|(iyz|Q)2WI2MVhnqzHgk?Qm~G4Jdbt+hMrf&agG>+b~Mf~Zv4nVfhVr_%Z^CH zCO2sdcvBDjJ0j6NqwyV=0iW7-367~es}MIa4vr0kN%u*wGI5hXh=`=*F9NcBtw__V z#ZIu1!?1VcJRIgv6R9pLpt#G=N@Qq0KRv*uyn*}K?JW{O>o0EFq|d`}4CkIYycG-X zrWPI`h$lDK&+UKR^FP=p|McpW=?^vgc(9{k%0FU;&SsZW@nzL~Wt7|L^nycGmhhUA zN*O7ayI833^(Z2cltMa!e~dtvC6cy==uW=~R3;Dt#TlA?tD>_m-M!J+nLVB=x>v5g zBk-9$nLW)>QuSx`=}2L(Xnj?zHoU^1`2Ik?4u6vC5d{X(bE;?($(%6bfrmz)tk0_v zW@UAEtDCg)hp7TvA4Kfv-gcvsp9E?8meJxm# z`n!{aNo8go!F!|)?j#fdbOb*L_wRn<>*@ivc69~+9L@gaGYIe@4UY1ky;SNvRC@;x z=~y>0Jid88!~_do-1Iy8+j!{1jcib6I{x6}6-xFtGsBN++(@p`{;=^upEE(+FGxfu zJshv%36UQYL15x2uH#4JNa)tPRrkV`P>2Y3wzeyFv2b%eiQY)158W>SihMg~Wg?F$ zeL*jO{AMOvz-fZVo6pH}ih2ajnbF3z(91~OovQH0mKNGvHoNuPH*(NZ)WUFon&~5Q z3>66+f57zS2Re@P9)(l%!?!=|9{Nv!drB{{*QqMh?v)El21a|)#tbRZgs`Q1u9xJr z>%&&PWUofpx}b;mM~`84Yr})HEATJrSfYjyZuG@BIOSGSye>D`^*K}LN!G?T3_U7#y1l~nSzdPHt|+G>vqQZH!(jdF{oRT{h?K9XeWS2-AI*7D7r<} zVI{&)n;-AkjEPdXi0$T{NdHcqGrfx*5`KV@TFCALUL6T2bz>dGB4S4^y(Hl|l31(j z=O=3)MRYF-y3-0aDaTdR)7Q8cLN8Oa*y_G8%B`}RbE+97#B?Ud3XTiEK-{G$%@yt9 zEF)D&w*Q>l;D@2kTkqW90>W9am3qmqZV=)WU{BEf*a}uD!5bXKNk=_$L)*X5v4hRk zCZTGU?U`I0S6%aQgX;$?mc@Fuj#yM6%@zGx@gYF%B^&Z8XT1SphQ$GlAZ$63E!oA& z>wRhfrs)s|pBWh8^#uKLM|aAU3&ycqZ96R?(0&!SokL8}R%F0vT*jSed4SD~@vB=* z>v_$DFG-UDVuvkZNN#_E3ea`XQ6PugL7*_v@z}q<80yu$44M1>{uc6dme81s^2XHC zh<6*t`Bmxs^@_q9JKU{hEX2-z9wabdzOf{n34iAaY;$f&0L~M7cq{OCo_IP~d4Ozn zy*wOU>^*-*ma_Co)xHP#1F&#d?as2xF)e-GZZWOjQ4wA(Bb0e+Y+y%mXEwY!wkehV zso^X7O@$gK;=ml{I@i@pw1dvU9IN1?Abz>+7;Q@GSotVI)3?T+JcOTBq$8guocXjE z2=VCTModBpBNN1BSH(=sOH>_gAIrQEnG*ZNDDNK@+gd(s82VZOMCk6^Tc)6wkG@y& z0G+yNIQMI1Mr=%I8jJ3fT{U^0Y-?fsfJL}AF<-fnU>kRUX1|#M_9W|c;y{>oiJU8o z%tMUFP~mH`?;4P`Fo^PbQv^ooTEameo1M`@KntDT1<3nYN24v_U`llu>UFc!LYJ8# zleS@Cj{5RwVJLno=v<_$o^H&7-xFBz5Erd~Hx9?j-P6=OF=24K{`R(hB}8s4r9tx_ zy;0nmPpt>uA`9P(GdHxa0e>>s*cO;KY>51><0Vu!_lLU~sbuPN@I?aURAsr7 z^PvS=BezBq?EYHNqX;oc9iy9_zDk1>7xQGlT`4(d@8)0ufCV?Kydw7- z53xz`6i$^!s(J7xBm_&EN)cZHek>|g`>xC=CGfYyrC#9Hm~RP!5EL<)zbkOQ|V=NlqyO5qM5b&2}*49 z2{vNKY*^o-sFKIn)$HMPp#I2`wlI4gTO^q%03OE5-mhXF9$d{xye~i4fnjcjbLjS+ ziiaRWWxB${+K{TmtJ>u44^yNqHtr1=qptR! zO`u;VG<*?i zzksM$ICFh==rp!P+(Msw4%2#07$=^F8$RwNWTqbKdG5Xb-hSn11jj|4)j<*A!BaXYrM==!E=_YcP1}Mi8%-Qe_liBmHN-%2R z5rCByZ6QD%M=xc!X^-_FC{+k6n(l9J+2+c&0lqLc3Zfc_Q17&CL;G+9gb zB$S4Z0IFP2Ss#~7>4uO~U^9~61N3xhc$3lN*HpHG=v?>mI=h;XCBv8l?0s60 z4@Ez#k)ddYz{fg^eO*@VwIxt5s9vU*`_fAvvy@<-K%n`&i&8Im` zS!mfgu&?nG_y+7q<*u>*vNLK=uESrT(HOtq-3ml;a`F8kCH84N_QYB2ZPBN8w_BO< zy;9I)(mrs#q@-T8qe&s z7jh1Ilo(BUOOQ__yrv{3NJzqysrBxB%$7gu5H#D?X?T-&1(2mpALc%XHK1!9iHVd zQjidido$RsdNY0)s*Y5E_LeX$*XTA)d|?JYaLyn$OdVhNqGW6l1GZzlQ8IH})T+f}Sd_A|7Ab8PX^WrasmOBlkv(+Res*a!tpBae z?8S#77P-1$N>z&Sk1<0{z%;DI2fAi#4qN%YOSJ^u{`1J3Vtd%9mu@-ti+oy5x9G)U zkQ(BK4oPp!GC^tX(@GK(NY(Nzk0K(`^F=cmsUP8n=;8?IQ-Fh;LXo~DbR(k>Qsx)3J|}YW`f~o_B?)B2{PA4hL`~y1qm$d9?;f;s3}7QH918flD@|i9tKyAsC=+|qSmq~w6C+h2(F5dZJHp=8f$aq zp>y~!GJ)-se$b4qHqK@I(CJYt?G!!1UM$ouxmNLunSF3=yJ3?@xM2;4{%S=E_2n75 zP%!eB>6nZaCSud)gq=dR=p;cG3+`N$`|%Pa{S*Y%`5;BHJtFSSOKr(mc5@Cgy&usq zePfvx?eC~Er9$XV=3S3MFo_akGmKM19LReKfM&#nCVE#NS+Z<{Mtq!m)7fE#ar{hk%+kGs5ofZxe*fLqbgexpK_KIEg2Yh13!SVj`JJz&mKv8UPC zj)5CXNm>v|bQX`L88KqR0~SvzrH@}!dn?X=H7?wxq5Jo-kwK z>TSCzW9r2)P!}7+o(asL1tW7b@|D-Kgl<=qTcZRi`+m_3)S*fRTsSIQ?VwF-?=%I| z&rHpEjhbG#J=Ie+j=*e66Y3|x_h!rfq1uP+1$LOoWfD@fJ;y3ns>;gU>1ExM+^38&s@H0Z zL|A;e3~D6?VZ3}2RV!#xy);)Y_=796Y0B0}hp%2Q-7TyBI$;m63d}s>5VnO?FzmF+ zdN6ocagPA|TH9=4Dfr61kb}j9K3Hkf{A9F(w|635tmL9n8Elzh4w3D~JK*XHO6o9$ zi^wUIXu^CeOU5&LnHt`$-`$4cn(hfh;nSXphqQKdK6-KP7A&XMOu$3*Y}TY%zd20# zU^VbNJ8^P8o-tGn$r;q+^U;yS;Xn^$2M0~yl=%R%*WcH$5LloY&MI=X z1m&Nc=NmU3Bg|fTy;_^#MG8^6*`if*ht15mJ_b6Rz0oYu-WS6~;-*0^7N$z86TXLI z&7IQcAVo1T@l|5aJ*BWU2~ipw+VO2@P%W%;ecj^qL2%|jhBKe+cjo^rAOA>Lf2FGV`ev?+JiwsB##_sa4iSV)Vy4$^B~C%v z0#(pYNuF|*%+2Ai`s;d?#ewJf1&NwPr*gJOGX%g8^fi>W?9_dhOjP9h$VQoxuQSQie$ntQ4DSk zGRng$ri=ip8L4%FRoEQm4;ytB4?#&mT?3w6LlymJK^}Mj zLvJ|SL8iy*u2<3t%mw^dl;kA)%7Q_K&(G^Oa_!5G`OhI=PL8BO>3F2xtSxc0?y zvo`%iEkhg%NC+Q^cq!b^B=1Pc=+Cd0a6UTH?fr-|@a|}x{{yW=k)pav?1s*EmezTs zE&*R#GcAwD>N@JZqH6xOZl|+~*={<_T5~HAsaKNCVK+`H2uv1pH+i3eQQwqHj7-#x zr)HGRb9Q53vtnx6b!?-YNoZDmA~fBm1^dm5x*v9-9qC2R1XL=p$4EjBSwWNn%%}4& z=|~E#@#G+n_A5Xm+()#Iq1nWhd^C-wI674l1R=E?+QB56H z7egQM+wU#pCPAK&b+3y-0Uu9Mxs*_&=f{V-e-xs&L3~8{hzo|yF~_W5Kfz?#y#^Amjn63?u-~mRhpk3jm`zF11c9-FVlxlKdf7_fl zQFbxOiK82~yMmE6d6SIXgGsWiNS>ubyfeM`kp>KsMhN1pfuUcLUVV^Kf~||GYg?&x|kB$hCu)PA(FBk15C?&Nl=k*L#<&j z_kg<@O(Nkd)6>t{mdUHQWLs&m6UseGWj8|Jc=%LuIE0xE+49J@{?ufSu2P#%;&cu6 z%C0pwT6LR0M~JkgG*cuwLlvE82KO&8Zsy;0~I&YBsx&JX()_V3RD!^No;42G!g==kYaPh}l^7^Tw+&$;zvUYT_wk*Qtspn}}Mxf}aoE!p#*R&pr>VyhN%OMjHr6yZiqo(6s z`+m@NAV~L7WTlknN8pXU!9(6F=wTjehTrBN!nT;%F+nce2k_x)4*s8gz{<_-zb^2z z&wgL>(|cT?Kc^H>E^(vxlJZ?xqaPS(mvKCvuLY<)egqODVOI-(M=@Lg%A!Wy!SPvy zdUf9`ZA^Nh0o8S74EZvrr1K>ZI$s%;o#=~_r{VE5?0u(zi*D`7*47%)C?WeJ_HAk) z1TF5_=vRiiSg8l{NNfyPS(pY4(c~K{b5pMb=oZp+F6LRw1YBBc*h^Puwy#jB4&O^i zf2~z;W6ET)avee)G)jO8a(3*U&x|{fq8r91;8#v#I;vjGZf<7O@Lau}wM3yw2;Y2K z1S{=%Hikm;5*~J=M9D{07m&lHN#6;+a&g20S*Mru8(~odjip~`fW<)zRjN~~z)JmT z)|klICL9$SXe!T0NJy~ckL!cm4c8tO8mAx;7wW-prTM2gKq?_0R3tQvm`IKYl9?8# zV){-532LM>Y^1%=hDroCKhLo_O?B>${emlI))odUnjqdiuByHf3--vD&i6?A>kSB0 z#kzx`SK+NM6cnL3W8ifmEvb{YByG?YY#LLIN#=`~{0XsgjGG@%)_yM}+AbosO47(6NZ-=|>i;@nl@ ze<9t%|EX|Cm%nR#SDyW4%mS}i;KqLwYwsf5l^%Z~Bw_#i_WzdeRo1nGuv zy!U-S@8$Y^|G@j6`OP`M^E+of=j?r+wf5T2+UiOusKfwt044wcpaVz=!9F@60RX6I z000pH6WK_{*$Hax1U1$6aj|wc=Js}U0OzA3v*rSj5%>Rh`w#Ac3hjQUx4ii6dYcjt zI&`1sg_o6KQnykYF>YX_dB4zSY+R*az4T{!tf5nj31`x9_-Mp@AVyv}&1@hox$MAw zZlK0!hVv?>(rxelm3)pYtgu2irY=58AeS6>cNcxU0YkZY+Jf7j{)V23RG=Q4e;vNB z`c1k#LPCbAoqeqg`RvQq{r?`$%}pD0co_~6 z+*3we#X-2KshhQfI}i8I{eRu_KiDV#()5ZKDr()lz_4BU%kaLFiMe=uX(cZy`8GQ3 zfFOn0I}I@fj8t=-EY$egl+RG)0^0*F2WIBPVmABej=w@Gq6mpa85+GR!&1*(Juujr z-BKR9RxG_E@SHfBI8K*S^kMaEk7F-yF3D5sUtoARzW1OO|0A~^B^FT;br|XW^kBpH zDhBfwr&UN3(puju!)ls@ayJt{z6_j9DgK5l5php>dn}!#&)vdis>-j=f$r#xQb)`7 zo?VSawwnmOkExYI*TI9AokUlDT&kIUn)DFDQ@)RKy-c}Bfrj;vy}>M>cS2a*<$XW; zVNsxxdBlkIPbUc$Pf9&N^hhhhNhkrBNZt-SfBK1+v)dC3XXhtBN3(zX3=(2UL!kU; zZxxykejrJ=+X39e=>#0u_6!vHoWz9u(&=pWUHe z&l3T>uP8)EaE_8lLe!^34!fdg&O--7D3})CD`{bJXhekDn_K1Iaqu$SiCV;8^l@er z3$LBBGEsyV6w^OAY#9rK*bno1@Y%bM&^S;!&iLV!qr?jY_!Q0A{> zDb!wYp6105$f>`9?~5|x(S=5+!)*l$9i{Cj#dY7`eMeu*FcX|xc5^Wt+(ph)67|B} z&nEC>?v5|ilD#fLIG|gYM1`(iGZ|rODI$@~JmHvt%UEvH@ zFHbh}E7sR>vAYA#SpS6VpZI*N-WIJ5tt$7aRx(X-Bq2D3p3HM0$U?iuyCX$$+#1h( zvM|mb*BKSGCxu$c!fV{ZjygV2lKdUH-V5wcBC&U0np;!dbbV2-%2m zFdG|6ALDg%3Y|TTvvmY&_BSR7)4zW8g_g*ewi4aBNrIZIAr)7~euzc&o#`b~tFa%b8LVBUTs2v+)5B zO1tz;*j1|cB1+BO2T=X^!E3hD#5VO@`IxGHUSeTS@k22tZgBP7d)wjME+<=3bSX(Z zj_DbPON`%fE@LA~`v$=^0|f7g|Kyyzt+|`El@`>^&dJ98XOv2MA*I^Qi!Xa3KMT*e z@A1f%tz>0eq997y!SFwz8(jX&b z-u#V0cwQZKT=zpw31K6xWooa=Owyi5o}U>a*rWSgcYyCAu6>~85@ryef~jrsg=5gJ zqLN^%vjyhyL^t>|(pg8WCjOi6mVChSpiUJ@O0Le;8!qlZ&e$J1lpn{uv)@nJy^&vj z3DK1e0}X(f^u|A2C~GB`mhlzg9kblDM3?i(ssKt&S1`m$0en*+^EGABU z1eqA=f6mhW2vbm7Ye#FIKlXn#d)v@RjY^I;&>^V|1$D$Q7#+CJ3P zy=^FUqUGs#-#KgSlb32v)DI{XmGStOOg~gC)U`jiQOsahDY^{JPB)kl^`_3v+r*Vz zoVyyf9(EkgUfOJE!;-;xAU`l}sNV4i8(%?YzQ|4+$Em?6l@ytgM71q#FX(=LD+ z4#8EABr9RxsO=GhOvIO zjMKz`W={xE%ehe&YsCq2p-9}H(nQVEG*XJdoR>Sk}0ND7q` zmXYbaO5I_9)NihhYoR;Qk+LXon+=hS=$uJU?1WK#Ia2d=s zbX5`9SAezfDxp)-W2MS5>dVgyBnPGWJCrRfm7xRY zd9lDq#NQ2iGc|g(8%HZ9+x{6=51YsxaxZYe|kmw8#@ICL@lFK>YdmVAJIup(l zaQ^O2$R%a<&8m%BfdBEz>T;Zk|5YD~DM7PVNRyp3<1}NqZ8%Gfpe4DU`xt>ufo*S^ z2(d8wJnd%{649<9q{rfqvYOwwapl&Z22Ec0##Xh}Zg6ZS`0=Y!FQTtk42Mnvgc>Sf zp)*&IeH^kYdMAu!l$>pJRhmh`3yM$Lm{STfLKa_3T83Q8TpnoV#|OMW!P<+T%llNj zg^r7s`sURLP2^_UQmRgvk7H`;Kp^ z`AvHLLy16xf~xwXi(C3V;!Iu`^qgNaG=pC;ycM?he4Zqp!G*NNC49vkX20ffflkH9 zt;yQDyWA3ckB46GEP&y?!rDZ7u`9tv@N77KkL^nikl(X5iu`K$9pS9aotY%z9q|_Y z+5CipF=5HW#Eu`Q`yxwD;&}RZh)k0W2%c-{>*a=BwEwu6W@cP~wLFMEp@Z^uCZ?uTzjypPd_FMn~ncIWe_& z#d4_$RIJfjbbe2{obyTVd8S=+pt)CF|KYnj8G0yWu z;`s#n?(~d%58HY~%qTlw;-|dadqa!$Z`Z2cysUXWVP7^iTa+AQUu@};B53$paBt+i zpa?#cAg~le0rwTu_o00}Z0MC%AmuwZ;n^fk*(w_)u1D5SK9A)N`sSf_UgD4miB5hV zy5aG~Y=mf#$WJ@Iv1F>_S^cjh|w5W+G z`ylDe2^cNVcFFQZ@MKZI0-O`p9jlH-btf#`{_Pu2=g?F$cCpo zEER)epuBpzMg_DLL*;D-vs0-Sn%9{rn!x3oDb2$G(*5C<6pzELma zI=2olgA`61E79oMR=zrWuRbJmM`M)rqj}m~a_Kqk#5X|l*}YTu>I2~BgVz4DSDxa6 z=^v+QqrG&L0tZYAHCfL?E6qmRPSUgOwejX(*XFpEH;_EB=&75~a&DlYR_|&lgr#AY zVw#WG8k+uad$}v=DC=VHG{b?a;omPvD^DhGL@PQXJoQpA4Z<&H-Pm@=U4Zo+8dGF^Q)k#Zx`RQf0vYCRVsA$*dGohHTo$5b^ zX@#GX;FVt}EBFHa$zlr@xw>h+WcH2LDaTeNew&JT=o?6jq5uGr2=x^A&wf>Pj zN77cIGZec8A)aU0!%I0yzl40N*LlGqk|nR#3NEBzBxRrzay8sKXL&uBhgNIZ`y$+| z_uR>ws_LDk%XXpTfb1dPh^!?D6u2(BF4}mY=L;X@p9o_!SUhEZ8!=!gpzMTwIblii zftN3(FU*C>F8I!;MZMKmIV&7Q3o?Tm_h>T|tp zrK956o2cK9?nX2P;LFt*augW)qBQ5(8|jYU`8M3`BfrT{2<`;34Zme=>GQ8PjYta1 z5a(wBal5w=CD))&*t0kWCnjPYaa2={oO(uTR1aj?=cFOclUFoz5GKctcd)qI## z6mp4<8*9TpDdts?cp zjR9d}ay&H{?F$-BKj9f{v5YsaEJLQIbR)FxIV|HFb)2ydV4T#di~pUm+Ccj zSPWuDzJZt?k^dv%|CQ=z>KnzC3*!IGd4s$cf^b{d`$w4#J{_o+ZXWL|Mw%q$no*XB(0LxucaeD^iw5r%kAf4LDK_ATip#mEq`LDD`1!n3?8&R zcH;mMW~2gKmFtoM<<%$q6DKB>qzwE%1f7UtFTeA#`4PSpsNR-vvMaV~_tsYSqaSi# zcH~+1^_bC9*D>(i0SNV(bX>xuHsgro(k>2`3KgeA7i<^xrGS}5>zB=xY{8v{j_VVE z9;3M5fri9I?_8A{o0Wcz8~HuwS-JX1UeEULhNL4PHlK3C1sYmVj>1Di)h;YI8jTz| zMbnTaz2;RED<)i2tIQvV$=eDkj0|AerOmb%XwGr$E5v{*P-`V++v+6Zjw8$ z9*hkpKbyh#j+8dY62~i4%$0`kp{k+TOY;W>sj;Z#O_(X}pBkGij1hIdwGAXxMM3>M z;tI;&2zsBOEvAUpxW(8MIew|;Sb3L(QaHudWx|$R z%A^a2#f=*8Z9i0SH-kC}D}~v7k*Am_WIf7E1c!|Ek!MT}giu-+KS3G`sK`HpQjBYy z_I(Meqx>bG5p0-Le3F#?NfL96lB%?pdB7krNj>aY!ECTA{}pRJj6}ZD%;rA4%ohA5 zS5#2bQl*C3HH$5DlUBSaPcdFc3SiEv4@C8gg1>nMOHwF@Rpe`^2ImQgCQ#avbDxjX zJq>h;Q)cQe?ZF3AhIvY1osxFTs>Kqs^47R82z=({y&CH$fF8S`^Y?cp6>neBNJ4-W zic>hJq|dDHprBy`IcMc#9LYI{u*b}KLEUED6#6(rcwkC&&m2teZt$URnR5CDd-s8v zIJ_p7Cq9l;%j)c@CIP^BOR7*EXj~WTu@O1vY}^06tC{XGNuLRzAtH~~V!#Sq8)`T} zz}HT{C9^YWu}o~_;oD;zt1zpXZxnTnzz@(p+eY(?0_`llQbym`B9Q!A0j3YNAhda! zD?=8n3#!e-MnV-WYjE5_Rg{@x&ufQ{ex*KiETfN(5sim{2YB%tk)PM|$jm@gQ;&sd zG@O?kU)U;Pm3JMNlrz0G;Tk0%x|u8tDzRHA#Zpq`R59|fiyI@7-$H(Z@&)dn=vcmU z{MEeet?%J`sTtsqCzo0cml`Ct|Cr?*HHLsS&W!8T)U<#na*xA>*FY_Y@mtz&ZtV3m+W=7v^sk<+TN*PR;9zvQv(|k6mBVr9}(s}Cptzcr#A0~$Y^r{T3sSJd~E?qqb zNEZ;}>ksZX&u+QP;N zK%HN|!m6mXQS`ZzSr#V>;K<_R#`0C|40az8PbzY8p;tQ;bW)(OaUC#T$3E$ls&MsW z)#45D2?qzuY@r)HXJ9@)3MmwJ7-ns{LC9GD6jrOOR`8h-dJVx(aTTG(TRB^*yE(hK z^H@5&S^ul5{9g$kF}8&yXef2_5{E9J-N^L&W>|&Zm6^HE>Z^%g!P*hJQiUUEC!are z-EPq2VTmnm^7h_I+xol_cT@3T2OMK=2D>&hA6T)S+~^o;@{+_Pm-B&n^2Spfl4&W) z!Smzdq}ajv?z&x?(-JzX=T<1GRnjlXX;>KhaLEbwS85aL z)g8x?L|5`KKi2o;Y!VE+YH>WvCHga0RgAf<+HMZLMHmE9)>vJ4zN-!xx!#OasJAa5 z6?>%(Q7N9CiYvP51fpj^GaIhM`$*GBn{#okGlzvO$=(s=a`#s=eExkF?m9&(BZcU@ zS;U{^&-}mzA)P|4-L$Ns(4QJA&#m*j+!l6D7G}9X-q*Y}b4Zj+mmR^$3Bifxq)k|I z*eDDr5;DXnMDbzqu|Q_x>1P#L1OrH;%96nR8x*8g62v2ZLvaz~LxKzh#qkmI0sOyG z+}y?GzYPD`XMb*)FW^oyKMRv+=XhZ|F_})RVUoHUg&a@6)BqHo>RF4BvMa&fQTAtB zr_!Kr<9bfdK)YxQ>SKykXSAFd1HVqnX@2#a`BD*@7VU-hQq?Ubc)SDRq*ZgYxw%3# zNXV|ozC}aqfbl$QaG9YtLR|6z3L67XDz+{|7{$86jIs6e_TvrxklE7DT&40xevY9AZk~Dt9|EmY{;GrQ$I2azhV^IPVsDFV7lxGTE{OxZ(T zInt8$yw)X96zK|>xqvkn%RZb*|L|m0P+k1!4QcDl0ub~_^&|6Hc;>K38P@e>^6F0! z{m+n%gv^DgLHvF((7(UmzmLCJ8lYpUB2Hoje$`DN^m^M~<2$~3nTZdWRPAru1t_woN<-QqUj zZQ=hHU@<~|K(yy}8Q?a+ZNcIf01qOpApm~K7q?As&%%D0CL<^92Z z6M`4xbbooLf5mZiB{T#F R0RSN4$Bdvc4&%?W{{gFy7QFxf literal 0 HcmV?d00001 diff --git a/hutool-poi/src/test/resources/priceIndex.xls b/hutool-poi/src/test/resources/priceIndex.xls new file mode 100644 index 0000000000000000000000000000000000000000..0746e7eb7d8d069a6dbf570ad5545fe3780853fc GIT binary patch literal 22016 zcmeG^30M?Iv%LqffG8j$hrogeD*|!}crJ2y0g8fG6cKe16~%y}{*i!JjDmguZM>gwuadgsD< zi#y-$vb#$-S2rRee<+QKAqy^mI0ou6CqxJlO#h)&Drq1g5dF{f7t+87kX5E?Loqzt zLclgRf*^rl48a6~DTF2v%pf#{&ztX=DayzVVke zF;Mq;^aoN2-U%tC?xY7k3ddJkmQJ}W*!k5ukCX5e0W8FTMi>m-)5rueijrdj4g83^ zB@qgc6r=RU6h-`15j185iG!L?hnk0zM7Sq5h5!r43it~kjdem9SXm852heH)?ZuL* zWFq_$Dc$#mR!E|Bf{-a>9Kke?Xm0C=aAv4$0n`x9(Mazhv_* zOC+S7`=0AQ0GkT|b%puC5O9tX0s-eBF%WQ$G6n+9Qzk>edC5!&=;MmP$>H1s%cP{H zrr=12X)H51CRV!Z*$*!vO3@>hKBD;4%YqODj9jQ5w=l`R^vRF>gq7zi3%%{lm#qZ( zJ?poJ1I&N}s|DCr2q+WQsTTyCJ7HbW=MI5j4q+q&tV=QkoL^zxu#VFqp!_)ym^z{_ z$5L1xFMUr(AdLN=7#fgcz0Mhx0|i467l3|X-k1qsk*TwhL zgD=(t{}t*Z=aol50`$O*dEo@`b>%_&53Dqu2&|F8B?XBA3>lty}wahRAR)D(Wb908eVe z${RL7F(#gk;0Vh=Simuby?xl_A>}A;4Qc@w%EsVOq0oYe2k|2$K?;Z-Qi^?ow6iLy zl1Bq@@p7ROAqi4Pu{SJ9VNseOWta1ivI}`gwMxi|C+Vck41~~-R~6NQ0Rg#O!!(ez z1C48#GPr8K8UUxH9ke5?frq=&m2BI2xJ(rv-qum<1p0>6uqKyMmKKT>G!Bbp6$JqJepV z4|0;;p|&Cjd7}g^sjpHBj-LIE5(d53Oh&_S^l^On`rt^F5C8u&T!7Z7zG!{oB#qJo zv(gmXQ~KA1x7PzV)&sZG19#E`_s|3P)C1QikF_5Bc6#9Y=+pSPv8Jsb8ebPq%cBdY z<NhYxPW z&{=L_vC?#=x5~mU;A`YVD1P?>QIrYNL6F&KIQm>M8WQa5bMhRL! zt-yuz$4DRBi;Evh&O!?dYg21z7urAI78m80aK%ntv_5e&T0mT2f_0(X2?wmScPZ#O01Fk4UM3ICJpPp4n zrJ0f?93nxGYeW&j8p`DvN-aeo5zJ6ltX48aj8w3!tSqHebxC+&#w-|+pe_?0Sf+$q zGxQ~YTC<8M?V53k$karz0)@m-PtD4HR@Y2d65O89NCF9MJE3*#=dUdZ*Vx)ZF}h_@ z65J=&(y^|w1#1<^CfVEqnVeRXLjgm0F9fEmZZTzr5+;QdjA^mmyLYSW%Cs25m{w$3 zj16O23}GldUb=e1*Gk%Yf?8?>VDyBYmN6sUYP&q*Qgj)|qQe2TjzH|}8bxr4wE1Tc!MA;lXp)8{anmBA zr#eb`!$h34h(H3nfUa>E0bHXKuA>TsT(+Zs>cr)e5% z!~s`FC>*SCR-&;1E)m#OU=cwYJVfYsX!?o3K8T74YC)&X7z0se3)Je#lnmrJ+;&h@ ztTpy(NrINg?dI&R&p2FNN9v&^9c(=DH{R(N4UJkIw1^Zc^bk=#DP&cVt8AQbH=@OG zTBTpv9kqx+g1^NY_8&NvZ-gWb6T$iYpAxBvYM2Nvy8e`iB1wCg<0=+cM1M-8BC25` zowbNSf?u)P6BDi^T^dJ3+c9&A;KJrltD{J2m#Rit68zQCCL-gw%(PynxOjKqqhn@{SL1{fDm#guJu%8h(1`1$zp@NwbgLk~+79|J|w``@ne z^ReXTrkanq zBI@Y1=lpzH^7G-dx7NIT*!ETt_4e#Gem*w*eE96motF>W-YTMQ+_=Hd$CjTDpS`u? z<-@kOiYVoS)%<+y`1$bJTYFwUYyu6&Bj}t#1K6~@z<-@i&KY!)*Bz`_pem;Em=EKW}ZEt@5d7D)Ihuc2R{CxQA zEr6E~+ur>AH(ki%tB;ttWE&`#N?L*21PN4^z`#H$BA;g%4!AM_LCb{3aGn@WcNitF zTD4TtP8(U&OerLogL^mBSPCnp@VN1et(n0&WHDWq9;dr<%A3PsODvDMXuCQzj9@3s z2qyBw3MzrsGNBm^{%VS7v0RE4YKpX#HiDv?h9cS~m!hSbB5jX)C@SS!D;}$4bk|Ts zD{RlHb#paEv^-wT84(#wR0PlCmrBsSuqo(EAh3sXW6$H6o^F6!Ww7swQehl7M^!x? z)Rs-4h3z;52uK+`;^YD(xC}NK+h$7u%S#(eC;>Z|x9E@MMWY!JkvzH*6?r2#hyWsH z4%HRW4n)ix$`%n9xr~Uo5?0YLhQk41Ts>1dkJOpOiEKem%)#Y3rF8a04Yx=c!w|s6 zjV5JCRmwA4tXp*hF_3g$Bp87e!K!;@8v&M7yXpo;ASz-F178e^rf`jkH%j_#&dSf0 zN>a582(5uE=HLzzPG3+{xO>Ia7r^#nq5LQ;z`exAUp))7t^ogUXa5{?66gmo% z%ccQxGBldOJRf91J7x4D=g49MvS{)dH-nW`w+q@20&u5@wjcj&5m<(!HYo!U2ox3@ z-Oq5q@)fjQh)F=UAz-?4Fb$PPYD{fA>VS46%=hZMR1&0JNYD!G2y@sy2X}Uuwkw3~ zA9c?OL^{hBzw57Sk1c=>=CEymZI9THTK0$ljXlySwhaj2jW(?zIuD11Aez5|hrboY zU!lbxUpq1Nz;{zx{6D@wh)Hm^G3cG+9fiiA_gC>HInTeEo1yEzvSF#JHolH%e3~_6 zXYIxiJ3w1AxlDsF9tSIe8yM=&gd9OZSP{&fjFJD&;J!}OoJufvq_Pht?2Wcny>2=f4 z!?_NZp0Ao7dwv7QcenNAqh}1K7@>pIr2MpGsbmDuW4k94Fi_0F4Q9q+<9iIoC*m72 zO@E+MOn;zLOn;zLd6FR{kHsM$m|4It4#v7saQsjKe@&SJe+R-?G=wp56cPWb;s=M5 z$HC!5{M(QQBMM++;dt^C@Q;CHBz3HI9`wJsj#vjnlAf$kOz~YKu_0?%Jn7L9-6SC! z;3)D2Rvwni1@#Vs-!Xv%0wIe6gycObrH(>3hkTq?jh?J9i>sXhi(`y-iGfJ*#EL~8 zxLn3a#0XF+NvO)e$JnYh)T3`dc*v3h6`)IeAfgl%%OvKf1A@`eniQNJMn^y*a$;o; zY%%o?i;#w5&@ zpCksJU&C{>#2NxbRF^dqAuA+PO;@mTFe4^XB(?$#BiNZdy49=%1_jazREioJ7M40` z3P_s}H)FCQJ>Glz#FVW|&h|awV;)i0>;0T+uRevlLnLh;%`VF;TR*e(%xYKb@~Yh% z2mW3cbtx{iSZbZ(dELKmZRh)GN1crJomf@4vCocIq3v9s*!PStx*gQ-`nEv}X4%h+-?kH~`NXZK#PW2E)!Xi@8KLNKu|x4K=_}J*%M&}BEJ!#%R%+|@prrHK1y65W9h!3FX5Q|+ znSmoJ`W{nSjeH}oX@0pPYgiU&LmS>5 z>hCUpD79X*+3>`@F!|Z!yBS?evSydh`)*?!$*}2@zu55EjeQA6VlHmza?aPjsMmg< zuPmE=dA;9}0oy~zTr4-g)FIPDsr|?q+^FE&qDZ=)Km2zRR%q9~ z?3+vLZd|LrKPTwt+g6nYk!C|Z3ysVcD$aR-d7yTt=SKVDUwXAV<(YD~ti^*z({0N4 zE^2WMFn08Rww)3GD<-0;=tsi$Wa@MWC$$l~Hl|AB8!u`OA0%5(WL)zeW1J;i(JLBv% zD#7)6!QJ~6wPptorM*cEJvzF=AaPvTj`+>*9Q%r_mj*n3JFVxO#ooWK&6k;3DB3=M zo9_93%VDR_CmTFFW8+>W`aSOHH+fExtDnVrxMU}~dhcqou{P!S^x3X6tNXY4tw#Lb zZSU@Z+XCAeyKH(=Ft?h z_=y1r4(C*^$T!|#)OY&f{!V-Mn>>uPiVGVWKYpQcWcBx-L|zb`HX<=03tfA6?RdUc zY!FitA6~wy`HTGUU2&JcamdYmSRW(3`o)#pwUXfSF=ZudovSP&ckD=b|MuM}{}*p^ zHf~(Jc3ro3)vsUPzuZ&)@JG)RqUAllzq{o{$=ly=^gCLzHnZ+=`QS-Md|s9)Utdox z`TUXL@ot~^{jL7ZW%*ipX8pN`S%1`?6x_a_oN~OU>#^g97xX*Q*>l#uN2OI2FISbU zJ|*jMa(9UD+l(KcpRE$!Ds27u__NWSr`HAd`qFgz#J`u`>y%p`Wm#4cbnm3l(YHsP z?KHzXU#vO%Wo`0`9?#3`D^A|*UQ^X!TFatA61y-HV)Z2st``>k#+Kj-J3sq*-)=%S#vw0(NZ?!ET4DA@4Xt!^8( z53H-*wP>!{tl~NEC9&;tuU&{*f3iAluH|^c_vYnQ5oy5>XDxVQYP>qnM=?TvGW3U( zNuLhcvE;SA)am(Nr;9U2wLa(?yYhV4$yE!U?iv@=qxXa3O?%rFR4qPb)yHbafz?xv zEnamr?8>w=qCP?2|L%Bf(4q6zvk!k$=vJhdRqXEn{7iL+immS)etslQ+~y~Jx?+U8 zYuDH#|7CmYY)0Ab`zH_1XgcWR-z{XGQTGQmNfTx)^SC)-|B@eHdP`(Cf}9hhl9p9v4$Oc3!EW{g>9>Rb zK4{2Z2T$*Ibvbd{c9HRlL3ifa zak7O&Yyv)6sa$j_BHb)w%P%3Zv2Bwde?8^>gA7q0w~(P_>q?(FbaS)4k{9DXEWGvf z`WC&`roPRQMV_Ab)Y;;p;M)3STe^q3K5%T)yKL~;_LoCqw>hsFHqlgk_v<#-42RCR zcBcEKXRi0l#YUH+4*RZm`8vyB-uZpU>*ER%T5a{b-sDo(+!M`HqsG2(I@@x>-tivU z&K9)`H?HXE?^-)(x7BXNhiw7hhi06+Hv7;QuRaamW7ERnl+}N=+Fo?zyA35@`TG3W zV!{{McFt|>?gkGqyV_)PW%SwYOVX>Vrk)QRu*5$+{kp%`!2a=r3KKGZS zqj#117k|C6x6S6;!+(fhFzkBk!pq0*%)@ z|7!bH=tj%W<#!}a!^1M&+A6{dp8o+8E#1-0>|*}9{V=N8!`SD}8Qth1ofP#K_B2Uq>g7Y`OWbaLd12cX(cIrUm<7sR`#*hVw%t?WJwNfs)#DDY{JOmP zXun&&$(!vYogRJl>6&8`e|VY~GQhg;g6o~f>>N2eFE!{{i?K^0WzlY~17zO5tJ0df z_gPkWVPRZ-_V(ZniwFJYvf=ygXAWQJRW~*Bo0kQGnWtQjjxztOlGv_@S)<(ZA=qxg z*!E7=@qhI1o_>9vfo%QhiaV_fHb;qn8g}T=$=gsdy6OU<7aZ}>H|GO1*2OaejEU4H zz?}qXO8bC5SyLizLGGkNQ_vP>TRNjOOZ{9*+!0N5bKsQxW(-PF#{Sf%_Qvt_H>}s6~P~OzGcmdU+{&RE#`>h7k-u93U5;xcu-R^FBsk0zEtvKK7)q$CQK zh3T*iOe1~Zj$e$3fp{u>tv~_rbXfl3mkRKol2TYM`oc2Nm*65F-nxrL@XyHrQL_Sk z11}(JOank`xKte?#Pt8>6&oHN4gCmLsH9hnG%7J&k=92sMH-WqnxsfiAD22QF-02f z>*wn~iC0Xv^cY8D`9}jXLRuK?#CQZ$bK&1HKG7t#w3nTo4JWP8Mc_>nfddJw5_D7s zk1;yX#sO}D2gabjvehvyc{ET>>`R|3YXewi#o3y?|HS?p8u%9eJ|O_>!M-a1zv3zI zRCR+pa=He0A?h?ZBLS07giR^9 zGj+$MyF`d5^>7)ll1I>tf}wDxq$w?<3Wgq(3q!Ny!hoX=3{S?Hf}f=>6+LtzTo`VS za$%?tE)4wz7lz{q7lw7@!cb>i82+V-3qxITVW?*=%p5c>=G4uC2WH6wYt941bEsT; zEqGwoJg}BLFdH72Ef36&2WHO$YsCX|;DI^vz??X+57g*SNO#aHA#_RrDKtDfC4>}O z9-R`AP@ur1#E^&9M&}to3hj(e8A1vzj7}Lr3T=x{Ng#z*MW>7*h4w_JOdy4pgejCF z4BDHJqws|U?77(fRv}bx3lS0#^d|wx6G93eIBcxxbxD4ddRZ!_@M`6Vr~huO;$cF< zseX(cv8G`#yP-MayhfKJ@a1q6f);3*8otvUg}^5qwwdWVqBg^*(pX2>|K;I`K1I8Z zXrahU64LOU<|v{#qNl++VvB{re2uOnY)A8O6zgylt2mld9QEI6j$(?VCGZdk4Z$iz zzyo83>X?x_CQ-+X)iD!w%v2pi-%3+4Q^%UBW6jht^anH^dI36ynx$jd-39l<4l-jfQvCzF2g(rG*m{D?b#1%}~`&&CwR*CNS{ zL2M+>$MSC^*+!%NBmA)$a4ex;&ygnJ*U)X_{|*s+@VjeuyPl~G8Pe360nHjODFiH! zlRhleQNqmCA1tHBOdUNXbn3X2F@qFo6dLmxCS0KU`E>B}^7Zld_4e@&^6{hB zivVx*C4eu^iC~CB9dN%rh;KQf$Bj-)O;1glB8`|nT9G34^YQVQs%nK@Idoi7QZO}s YQ2E}m_?pr?n7r<|`_o1X@fSw?Z{{bFdH?_b literal 0 HcmV?d00001 diff --git a/hutool-script/pom.xml b/hutool-script/pom.xml new file mode 100644 index 000000000..40067c363 --- /dev/null +++ b/hutool-script/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + jar + + + cn.hutool + hutool-parent + 4.6.2-SNAPSHOT + + + hutool-script + ${project.artifactId} + Hutool 脚本执行封装 + + + + cn.hutool + hutool-core + ${project.parent.version} + + + diff --git a/hutool-script/src/main/java/cn/hutool/script/FullSupportScriptEngine.java b/hutool-script/src/main/java/cn/hutool/script/FullSupportScriptEngine.java new file mode 100644 index 000000000..8ee1baeae --- /dev/null +++ b/hutool-script/src/main/java/cn/hutool/script/FullSupportScriptEngine.java @@ -0,0 +1,159 @@ +package cn.hutool.script; + +import java.io.Reader; + +import javax.script.Bindings; +import javax.script.Compilable; +import javax.script.CompiledScript; +import javax.script.Invocable; +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineFactory; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; + +import cn.hutool.core.util.StrUtil; + +/** + * 全功能引擎类,支持Compilable和Invocable + * + * @author Looly + * + */ +public class FullSupportScriptEngine implements ScriptEngine, Compilable, Invocable { + + ScriptEngine engine; + + /** + * 构造 + * + * @param engine 脚本引擎 + */ + public FullSupportScriptEngine(ScriptEngine engine) { + this.engine = engine; + } + + /** + * 构造 + * + * @param nameOrExtOrMime 脚本名或者脚本语言扩展名或者MineType + */ + public FullSupportScriptEngine(String nameOrExtOrMime) { + final ScriptEngineManager manager = new ScriptEngineManager(); + ScriptEngine engine = manager.getEngineByName(nameOrExtOrMime); + if (null == engine) { + engine = manager.getEngineByExtension(nameOrExtOrMime); + } + if (null == engine) { + engine = manager.getEngineByMimeType(nameOrExtOrMime); + } + if (null == engine) { + throw new NullPointerException(StrUtil.format("Script for [{}] not support !", nameOrExtOrMime)); + } + this.engine = engine; + } + + // ----------------------------------------------------------------------------------------------- Invocable + @Override + public Object invokeMethod(Object thiz, String name, Object... args) throws ScriptException, NoSuchMethodException { + return ((Invocable) engine).invokeMethod(thiz, name, args); + } + + @Override + public Object invokeFunction(String name, Object... args) throws ScriptException, NoSuchMethodException { + return ((Invocable) engine).invokeFunction(name, args); + } + + @Override + public T getInterface(Class clasz) { + return ((Invocable) engine).getInterface(clasz); + } + + @Override + public T getInterface(Object thiz, Class clasz) { + return ((Invocable) engine).getInterface(thiz, clasz); + } + + // ----------------------------------------------------------------------------------------------- Compilable + @Override + public CompiledScript compile(String script) throws ScriptException { + return ((Compilable) engine).compile(script); + } + + @Override + public CompiledScript compile(Reader script) throws ScriptException { + return ((Compilable) engine).compile(script); + } + + // ----------------------------------------------------------------------------------------------- ScriptEngine + @Override + public Object eval(String script, ScriptContext context) throws ScriptException { + return engine.eval(script, context); + } + + @Override + public Object eval(Reader reader, ScriptContext context) throws ScriptException { + return engine.eval(reader, context); + } + + @Override + public Object eval(String script) throws ScriptException { + return engine.eval(script); + } + + @Override + public Object eval(Reader reader) throws ScriptException { + return engine.eval(reader); + } + + @Override + public Object eval(String script, Bindings n) throws ScriptException { + return engine.eval(script, n); + } + + @Override + public Object eval(Reader reader, Bindings n) throws ScriptException { + return engine.eval(reader, n); + } + + @Override + public void put(String key, Object value) { + engine.put(key, value); + } + + @Override + public Object get(String key) { + return engine.get(key); + } + + @Override + public Bindings getBindings(int scope) { + return engine.getBindings(scope); + } + + @Override + public void setBindings(Bindings bindings, int scope) { + engine.setBindings(bindings, scope); + } + + @Override + public Bindings createBindings() { + return engine.createBindings(); + } + + @Override + public ScriptContext getContext() { + return engine.getContext(); + } + + @Override + public void setContext(ScriptContext context) { + engine.setContext(context); + } + + @Override + public ScriptEngineFactory getFactory() { + return engine.getFactory(); + } + +} diff --git a/hutool-script/src/main/java/cn/hutool/script/JavaScriptEngine.java b/hutool-script/src/main/java/cn/hutool/script/JavaScriptEngine.java new file mode 100644 index 000000000..d6f0b9d46 --- /dev/null +++ b/hutool-script/src/main/java/cn/hutool/script/JavaScriptEngine.java @@ -0,0 +1,136 @@ +package cn.hutool.script; + +import java.io.Reader; + +import javax.script.Bindings; +import javax.script.Compilable; +import javax.script.CompiledScript; +import javax.script.Invocable; +import javax.script.ScriptContext; +import javax.script.ScriptEngineFactory; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; + +/** + * Javascript引擎类 + * @author Looly + * + */ +public class JavaScriptEngine extends FullSupportScriptEngine{ + + public JavaScriptEngine() { + super(new ScriptEngineManager().getEngineByName("javascript")); + } + + /** + * 引擎实例 + * @return 引擎实例 + */ + public static JavaScriptEngine instance(){ + return new JavaScriptEngine(); + } + + //----------------------------------------------------------------------------------------------- Invocable + @Override + public Object invokeMethod(Object thiz, String name, Object... args) throws ScriptException, NoSuchMethodException { + return ((Invocable)engine).invokeMethod(thiz, name, args); + } + + @Override + public Object invokeFunction(String name, Object... args) throws ScriptException, NoSuchMethodException { + return ((Invocable)engine).invokeFunction(name, args); + } + + @Override + public T getInterface(Class clasz) { + return ((Invocable)engine).getInterface(clasz); + } + + @Override + public T getInterface(Object thiz, Class clasz) { + return ((Invocable)engine).getInterface(thiz, clasz); + } + + //----------------------------------------------------------------------------------------------- Compilable + @Override + public CompiledScript compile(String script) throws ScriptException { + return ((Compilable)engine).compile(script); + } + + @Override + public CompiledScript compile(Reader script) throws ScriptException { + return ((Compilable)engine).compile(script); + } + + //----------------------------------------------------------------------------------------------- ScriptEngine + @Override + public Object eval(String script, ScriptContext context) throws ScriptException { + return engine.eval(script, context); + } + + @Override + public Object eval(Reader reader, ScriptContext context) throws ScriptException { + return engine.eval(reader, context); + } + + @Override + public Object eval(String script) throws ScriptException { + return engine.eval(script); + } + + @Override + public Object eval(Reader reader) throws ScriptException { + return engine.eval(reader); + } + + @Override + public Object eval(String script, Bindings n) throws ScriptException { + return engine.eval(script, n); + } + + @Override + public Object eval(Reader reader, Bindings n) throws ScriptException { + return engine.eval(reader, n); + } + + @Override + public void put(String key, Object value) { + engine.put(key, value); + } + + @Override + public Object get(String key) { + return engine.get(key); + } + + @Override + public Bindings getBindings(int scope) { + return engine.getBindings(scope); + } + + @Override + public void setBindings(Bindings bindings, int scope) { + engine.setBindings(bindings, scope); + } + + @Override + public Bindings createBindings() { + return engine.createBindings(); + } + + @Override + public ScriptContext getContext() { + return engine.getContext(); + } + + @Override + public void setContext(ScriptContext context) { + engine.setContext(context); + } + + @Override + public ScriptEngineFactory getFactory() { + return engine.getFactory(); + } + +} diff --git a/hutool-script/src/main/java/cn/hutool/script/ScriptRuntimeException.java b/hutool-script/src/main/java/cn/hutool/script/ScriptRuntimeException.java new file mode 100644 index 000000000..fe530ee02 --- /dev/null +++ b/hutool-script/src/main/java/cn/hutool/script/ScriptRuntimeException.java @@ -0,0 +1,127 @@ +package cn.hutool.script; + +import javax.script.ScriptException; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 脚本运行时异常 + * + * @author xiaoleilu + */ +public class ScriptRuntimeException extends RuntimeException { + private static final long serialVersionUID = 8247610319171014183L; + + private String fileName; + private int lineNumber = -1; + private int columnNumber = -1; + + public ScriptRuntimeException(Throwable e) { + super(ExceptionUtil.getMessage(e), e); + } + + public ScriptRuntimeException(String message) { + super(message); + } + + public ScriptRuntimeException(String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params)); + } + + public ScriptRuntimeException(String message, Throwable throwable) { + super(message, throwable); + } + + public ScriptRuntimeException(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } + + /** + * Creates a ScriptException with message, filename and linenumber to be used in error messages. + * + * @param message The string to use in the message + * + * @param fileName The file or resource name describing the location of a script error causing the ScriptException to be thrown. + * + * @param lineNumber A line number describing the location of a script error causing the ScriptException to be thrown. + */ + public ScriptRuntimeException(String message, String fileName, int lineNumber) { + super(message); + this.fileName = fileName; + this.lineNumber = lineNumber; + this.columnNumber = -1; + } + + /** + * ScriptException constructor specifying message, filename, line number and column number. + * + * @param message The message. + * @param fileName The filename + * @param lineNumber the line number. + * @param columnNumber the column number. + */ + public ScriptRuntimeException(String message, String fileName, int lineNumber, int columnNumber) { + super(message); + this.fileName = fileName; + this.lineNumber = lineNumber; + this.columnNumber = columnNumber; + } + + public ScriptRuntimeException(ScriptException e) { + super(e); + this.fileName = e.getFileName(); + this.lineNumber = e.getLineNumber(); + this.columnNumber = e.getColumnNumber(); + } + + /** + * Returns a message containing the String passed to a constructor as well as line and column numbers and filename if any of these are known. + * + * @return The error message. + */ + @Override + public String getMessage() { + String ret = super.getMessage(); + if (fileName != null) { + ret += (" in " + fileName); + if (lineNumber != -1) { + ret += " at line number " + lineNumber; + } + + if (columnNumber != -1) { + ret += " at column number " + columnNumber; + } + } + + return ret; + } + + /** + * Get the line number on which an error occurred. + * + * @return The line number. Returns -1 if a line number is unavailable. + */ + public int getLineNumber() { + return lineNumber; + } + + /** + * Get the column number on which an error occurred. + * + * @return The column number. Returns -1 if a column number is unavailable. + */ + public int getColumnNumber() { + return columnNumber; + } + + /** + * Get the source of the script causing the error. + * + * @return The file name of the script or some other string describing the script source. May return some implementation-defined string such as <unknown> if a description of the + * source is unavailable. + */ + public String getFileName() { + return fileName; + } +} diff --git a/hutool-script/src/main/java/cn/hutool/script/ScriptUtil.java b/hutool-script/src/main/java/cn/hutool/script/ScriptUtil.java new file mode 100644 index 000000000..559a58ea9 --- /dev/null +++ b/hutool-script/src/main/java/cn/hutool/script/ScriptUtil.java @@ -0,0 +1,119 @@ +package cn.hutool.script; + +import javax.script.Bindings; +import javax.script.Compilable; +import javax.script.CompiledScript; +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; + +/** + * 脚本工具类 + * + * @author Looly + * + */ +public class ScriptUtil { + + /** + * 获得 {@link ScriptEngine} 实例 + * + * @param name 脚本名称 + * @return {@link ScriptEngine} 实例 + */ + public static ScriptEngine getScript(String name) { + return new ScriptEngineManager().getEngineByName(name); + } + + /** + * 获得 Javascript引擎 {@link JavaScriptEngine} + * + * @return {@link JavaScriptEngine} + */ + public static JavaScriptEngine getJavaScriptEngine() { + return new JavaScriptEngine(); + } + + /** + * 编译脚本 + * + * @param script 脚本内容 + * @return {@link CompiledScript} + * @throws ScriptRuntimeException 脚本异常 + * @since 3.2.0 + */ + public static Object eval(String script) throws ScriptRuntimeException { + try { + return compile(script).eval(); + } catch (ScriptException e) { + throw new ScriptRuntimeException(e); + } + } + + /** + * 编译脚本 + * + * @param script 脚本内容 + * @param context 脚本上下文 + * @return {@link CompiledScript} + * @throws ScriptRuntimeException 脚本异常 + * @since 3.2.0 + */ + public static Object eval(String script, ScriptContext context) throws ScriptRuntimeException { + try { + return compile(script).eval(context); + } catch (ScriptException e) { + throw new ScriptRuntimeException(e); + } + } + + /** + * 编译脚本 + * + * @param script 脚本内容 + * @param bindings 绑定的参数 + * @return {@link CompiledScript} + * @throws ScriptRuntimeException 脚本异常 + * @since 3.2.0 + */ + public static Object eval(String script, Bindings bindings) throws ScriptRuntimeException { + try { + return compile(script).eval(bindings); + } catch (ScriptException e) { + throw new ScriptRuntimeException(e); + } + } + + /** + * 编译脚本 + * + * @param script 脚本内容 + * @return {@link CompiledScript} + * @throws ScriptRuntimeException 脚本异常 + * @since 3.2.0 + */ + public static CompiledScript compile(String script) throws ScriptRuntimeException { + try { + return compile(getJavaScriptEngine(), script); + } catch (ScriptException e) { + throw new ScriptRuntimeException(e); + } + } + + /** + * 编译脚本 + * + * @param engine 引擎 + * @param script 脚本内容 + * @return {@link CompiledScript} + * @throws ScriptException 脚本异常 + */ + public static CompiledScript compile(ScriptEngine engine, String script) throws ScriptException { + if (engine instanceof Compilable) { + Compilable compEngine = (Compilable) engine; + return compEngine.compile(script); + } + return null; + } +} diff --git a/hutool-script/src/main/java/cn/hutool/script/package-info.java b/hutool-script/src/main/java/cn/hutool/script/package-info.java new file mode 100644 index 000000000..ccd540c6d --- /dev/null +++ b/hutool-script/src/main/java/cn/hutool/script/package-info.java @@ -0,0 +1,7 @@ +/** + * Script模块主要针对Java的javax.script封装,可以运行Javascript脚本。 + * + * @author looly + * + */ +package cn.hutool.script; \ No newline at end of file diff --git a/hutool-script/src/test/java/cn/hutool/script/test/ScriptUtilTest.java b/hutool-script/src/test/java/cn/hutool/script/test/ScriptUtilTest.java new file mode 100644 index 000000000..fda465b73 --- /dev/null +++ b/hutool-script/src/test/java/cn/hutool/script/test/ScriptUtilTest.java @@ -0,0 +1,33 @@ +package cn.hutool.script.test; + +import javax.script.CompiledScript; +import javax.script.ScriptException; + +import org.junit.Test; + +import cn.hutool.script.ScriptRuntimeException; +import cn.hutool.script.ScriptUtil; + +/** + * 脚本单元测试类 + * + * @author looly + * + */ +public class ScriptUtilTest { + + @Test + public void compileTest() { + CompiledScript script = ScriptUtil.compile("print('Script test!');"); + try { + script.eval(); + } catch (ScriptException e) { + throw new ScriptRuntimeException(e); + } + } + + @Test + public void evalTest() { + ScriptUtil.eval("print('Script test!');"); + } +} diff --git a/hutool-setting/pom.xml b/hutool-setting/pom.xml new file mode 100644 index 000000000..39616196a --- /dev/null +++ b/hutool-setting/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + jar + + + cn.hutool + hutool-parent + 4.6.2-SNAPSHOT + + + hutool-setting + ${project.artifactId} + Hutool 配置文件增强 + + + + cn.hutool + hutool-core + ${project.parent.version} + + + cn.hutool + hutool-log + ${project.parent.version} + + + diff --git a/hutool-setting/src/main/java/cn/hutool/setting/AbsSetting.java b/hutool-setting/src/main/java/cn/hutool/setting/AbsSetting.java new file mode 100644 index 000000000..9b12531c0 --- /dev/null +++ b/hutool-setting/src/main/java/cn/hutool/setting/AbsSetting.java @@ -0,0 +1,293 @@ +package cn.hutool.setting; + +import java.io.Serializable; +import java.lang.reflect.Type; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.copier.CopyOptions; +import cn.hutool.core.bean.copier.ValueProvider; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.getter.OptNullBasicTypeFromStringGetter; +import cn.hutool.core.util.StrUtil; +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; + +/** + * Setting抽象类 + * + * @author Looly + * + */ +public abstract class AbsSetting extends OptNullBasicTypeFromStringGetter implements Serializable{ + private static final long serialVersionUID = 6200156302595905863L; + private final static Log log = LogFactory.get(); + + /** 数组类型值默认分隔符 */ + public final static String DEFAULT_DELIMITER = ","; + /** 默认分组 */ + public final static String DEFAULT_GROUP = StrUtil.EMPTY; + + @Override + public String getStr(String key, String defaultValue) { + return getStr(key, DEFAULT_GROUP, defaultValue); + } + + /** + * 获得字符串类型值 + * + * @param key KEY + * @param group 分组 + * @param defaultValue 默认值 + * @return 值或默认值 + */ + public String getStr(String key, String group, String defaultValue) { + final String value = getByGroup(key, group); + if (StrUtil.isBlank(value)) { + return defaultValue; + } + return value; + } + + /** + * 获得指定分组的键对应值 + * + * @param key 键 + * @param group 分组 + * @return 值 + */ + public abstract String getByGroup(String key, String group); + + // --------------------------------------------------------------- Get + /** + * 带有日志提示的get,如果没有定义指定的KEY,则打印debug日志 + * + * @param key 键 + * @return 值 + */ + public String getWithLog(String key) { + final String value = getStr(key); + if (value == null) { + log.debug("No key define for [{}]!", key); + } + return value; + } + + /** + * 带有日志提示的get,如果没有定义指定的KEY,则打印debug日志 + * + * @param key 键 + * @param group 分组 + * @return 值 + */ + public String getByGroupWithLog(String key, String group) { + final String value = getByGroup(key, group); + if (value == null) { + log.debug("No key define for [{}] of group [{}] !", key, group); + } + return value; + } + + // --------------------------------------------------------------- Get string array + /** + * 获得数组型 + * + * @param key 属性名 + * @return 属性值 + */ + public String[] getStrings(String key) { + return getStrings(key, null); + } + + /** + * 获得数组型 + * + * @param key 属性名 + * @param defaultValue 默认的值 + * @return 属性值 + */ + public String[] getStringsWithDefault(String key, String[] defaultValue) { + String[] value = getStrings(key, null); + if (null == value) { + value = defaultValue; + } + + return value; + } + + /** + * 获得数组型 + * + * @param key 属性名 + * @param group 分组名 + * @return 属性值 + */ + public String[] getStrings(String key, String group) { + return getStrings(key, group, DEFAULT_DELIMITER); + } + + /** + * 获得数组型 + * + * @param key 属性名 + * @param group 分组名 + * @param delimiter 分隔符 + * @return 属性值 + */ + public String[] getStrings(String key, String group, String delimiter) { + final String value = getByGroup(key, group); + if (StrUtil.isBlank(value)) { + return null; + } + return StrUtil.split(value, delimiter); + } + + // --------------------------------------------------------------- Get int + /** + * 获取数字型型属性值 + * + * @param key 属性名 + * @param group 分组名 + * @return 属性值 + */ + public Integer getInt(String key, String group) { + return getInt(key, group, null); + } + + /** + * 获取数字型型属性值 + * + * @param key 属性名 + * @param group 分组名 + * @param defaultValue 默认值 + * @return 属性值 + */ + public Integer getInt(String key, String group, Integer defaultValue) { + return Convert.toInt(getByGroup(key, group), defaultValue); + } + + // --------------------------------------------------------------- Get bool + /** + * 获取波尔型属性值 + * + * @param key 属性名 + * @param group 分组名 + * @return 属性值 + */ + public Boolean getBool(String key, String group) { + return getBool(key, group, null); + } + + /** + * 获取波尔型型属性值 + * + * @param key 属性名 + * @param group 分组名 + * @param defaultValue 默认值 + * @return 属性值 + */ + public Boolean getBool(String key, String group, Boolean defaultValue) { + return Convert.toBool(getByGroup(key, group), defaultValue); + } + + // --------------------------------------------------------------- Get long + /** + * 获取long类型属性值 + * + * @param key 属性名 + * @param group 分组名 + * @return 属性值 + */ + public Long getLong(String key, String group) { + return getLong(key, group, null); + } + + /** + * 获取long类型属性值 + * + * @param key 属性名 + * @param group 分组名 + * @param defaultValue 默认值 + * @return 属性值 + */ + public Long getLong(String key, String group, Long defaultValue) { + return Convert.toLong(getByGroup(key, group), defaultValue); + } + + // --------------------------------------------------------------- Get char + /** + * 获取char类型属性值 + * + * @param key 属性名 + * @param group 分组名 + * @return 属性值 + */ + public Character getChar(String key, String group) { + final String value = getByGroup(key, group); + if (StrUtil.isBlank(value)) { + return null; + } + return value.charAt(0); + } + + // --------------------------------------------------------------- Get double + /** + * 获取double类型属性值 + * + * @param key 属性名 + * @param group 分组名 + * @return 属性值 + */ + public Double getDouble(String key, String group) { + return getDouble(key, group, null); + } + + /** + * 获取double类型属性值 + * + * @param key 属性名 + * @param group 分组名 + * @param defaultValue 默认值 + * @return 属性值 + */ + public Double getDouble(String key, String group, Double defaultValue) { + return Convert.toDouble(getByGroup(key, group), defaultValue); + } + + /** + * 将setting中的键值关系映射到对象中,原理是调用对象对应的set方法
+ * 只支持基本类型的转换 + * + * @param group 分组 + * @param bean Bean对象 + * @return Bean + */ + public Object toBean(final String group, Object bean) { + return BeanUtil.fillBean(bean, new ValueProvider(){ + + @Override + public Object value(String key, Type valueType) { + final String value = getByGroup(key, group); +// if (null != value) { +// log.debug("Parse setting to object field [{}={}]", key, value); +// } + return value; + } + + @Override + public boolean containsKey(String key) { + return null != getByGroup(key, group); + } + }, CopyOptions.create()); + } + + /** + * 将setting中的键值关系映射到对象中,原理是调用对象对应的set方法
+ * 只支持基本类型的转换 + * + * @param bean Bean + * @return Bean + */ + public Object toBean(Object bean) { + return toBean(null, bean); + } +} diff --git a/hutool-setting/src/main/java/cn/hutool/setting/GroupedMap.java b/hutool-setting/src/main/java/cn/hutool/setting/GroupedMap.java new file mode 100644 index 000000000..1ce948ca4 --- /dev/null +++ b/hutool-setting/src/main/java/cn/hutool/setting/GroupedMap.java @@ -0,0 +1,321 @@ +package cn.hutool.setting; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; +import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; + +/** + * 基于分组的Map
+ * 此对象方法线程安全 + * + * @author looly + * @since 4.0.11 + */ +public class GroupedMap extends LinkedHashMap> { + private static final long serialVersionUID = -7777365130776081931L; + + private final ReentrantReadWriteLock cacheLock = new ReentrantReadWriteLock(); + private final ReadLock readLock = cacheLock.readLock(); + private final WriteLock writeLock = cacheLock.writeLock(); + private int size = -1; + + /** + * 获取分组对应的值,如果分组不存在或者值不存在则返回null + * + * @param group 分组 + * @param key 键 + * @return 值,如果分组不存在或者值不存在则返回null + */ + public String get(String group, String key) { + readLock.lock(); + try { + LinkedHashMap map = this.get(StrUtil.nullToEmpty(group)); + if (MapUtil.isNotEmpty(map)) { + return map.get(key); + } + } finally { + readLock.unlock(); + } + return null; + } + + @Override + public LinkedHashMap get(Object key) { + readLock.lock(); + try { + return super.get(key); + } finally { + readLock.unlock(); + } + } + + /** + * 总的键值对数 + * + * @return 总键值对数 + */ + public int size() { + writeLock.lock(); + try { + if (this.size < 0) { + this.size = 0; + for (LinkedHashMap value : this.values()) { + this.size += value.size(); + } + } + } finally { + writeLock.unlock(); + } + return this.size; + } + + /** + * 将键值对加入到对应分组中 + * + * @param group 分组 + * @param key 键 + * @param value 值 + * @return 此key之前存在的值,如果没有返回null + */ + public String put(String group, String key, String value) { + group = StrUtil.nullToEmpty(group).trim(); + writeLock.lock(); + try { + LinkedHashMap valueMap = this.get(group); + if (null == valueMap) { + valueMap = new LinkedHashMap<>(); + this.put(group, valueMap); + } + this.size = -1; + return valueMap.put(key, value); + } finally { + writeLock.unlock(); + } + } + + /** + * 加入多个键值对到某个分组下 + * + * @param group 分组 + * @param m 键值对 + * @return this + */ + public GroupedMap putAll(String group, Map m) { + for (Entry entry : m.entrySet()) { + this.put(group, entry.getKey(), entry.getValue()); + } + return this; + } + + /** + * 从指定分组中删除指定值 + * + * @param group 分组 + * @param key 键 + * @return 被删除的值,如果值不存在,返回null + */ + public String remove(String group, String key) { + group = StrUtil.nullToEmpty(group).trim(); + writeLock.lock(); + try { + final LinkedHashMap valueMap = this.get(group); + if (MapUtil.isNotEmpty(valueMap)) { + return valueMap.remove(key); + } + } finally { + writeLock.unlock(); + } + return null; + } + + /** + * 某个分组对应的键值对是否为空 + * + * @param group 分组 + * @return 是否为空 + */ + public boolean isEmpty(String group) { + group = StrUtil.nullToEmpty(group).trim(); + readLock.lock(); + try { + final LinkedHashMap valueMap = this.get(group); + if (MapUtil.isNotEmpty(valueMap)) { + return valueMap.isEmpty(); + } + } finally { + readLock.unlock(); + } + return true; + } + + /** + * 是否为空,如果多个分组同时为空,也按照空处理 + * + * @return 是否为空,如果多个分组同时为空,也按照空处理 + */ + @Override + public boolean isEmpty() { + return this.size() == 0; + } + + /** + * 指定分组中是否包含指定key + * + * @param group 分组 + * @param key 键 + * @return 是否包含key + */ + public boolean containsKey(String group, String key) { + group = StrUtil.nullToEmpty(group).trim(); + readLock.lock(); + try { + final LinkedHashMap valueMap = this.get(group); + if (MapUtil.isNotEmpty(valueMap)) { + return valueMap.containsKey(key); + } + } finally { + readLock.unlock(); + } + return false; + } + + /** + * 指定分组中是否包含指定值 + * + * @param group 分组 + * @param value 值 + * @return 是否包含值 + */ + public boolean containsValue(String group, String value) { + group = StrUtil.nullToEmpty(group).trim(); + readLock.lock(); + try { + final LinkedHashMap valueMap = this.get(group); + if (MapUtil.isNotEmpty(valueMap)) { + return valueMap.containsValue(value); + } + } finally { + readLock.unlock(); + } + return false; + } + + /** + * 清除指定分组下的所有键值对 + * + * @param group 分组 + * @return this + */ + public GroupedMap clear(String group) { + group = StrUtil.nullToEmpty(group).trim(); + writeLock.lock(); + try { + final LinkedHashMap valueMap = this.get(group); + if (MapUtil.isNotEmpty(valueMap)) { + valueMap.clear(); + } + } finally { + writeLock.unlock(); + } + return this; + } + + @Override + public Set keySet() { + readLock.lock(); + try { + return super.keySet(); + } finally { + readLock.unlock(); + } + } + + /** + * 指定分组所有键的Set + * + * @param group 分组 + * @return 键Set + */ + public Set keySet(String group) { + group = StrUtil.nullToEmpty(group).trim(); + readLock.lock(); + try { + final LinkedHashMap valueMap = this.get(group); + if (MapUtil.isNotEmpty(valueMap)) { + return valueMap.keySet(); + } + } finally { + readLock.unlock(); + } + return Collections.emptySet(); + } + + /** + * 指定分组下所有值 + * + * @param group 分组 + * @return 值 + */ + public Collection values(String group) { + group = StrUtil.nullToEmpty(group).trim(); + readLock.lock(); + try { + final LinkedHashMap valueMap = this.get(group); + if (MapUtil.isNotEmpty(valueMap)) { + return valueMap.values(); + } + } finally { + readLock.unlock(); + } + return Collections.emptyList(); + } + + @Override + public Set>> entrySet() { + readLock.lock(); + try { + return super.entrySet(); + } finally { + readLock.unlock(); + } + } + + /** + * 指定分组下所有键值对 + * + * @param group 分组 + * @return 键值对 + */ + public Set> entrySet(String group) { + group = StrUtil.nullToEmpty(group).trim(); + readLock.lock(); + try { + final LinkedHashMap valueMap = this.get(group); + if (MapUtil.isNotEmpty(valueMap)) { + return valueMap.entrySet(); + } + } finally { + readLock.unlock(); + } + return Collections.emptySet(); + } + + @Override + public String toString() { + readLock.lock(); + try { + return super.toString(); + } finally { + readLock.unlock(); + } + } +} diff --git a/hutool-setting/src/main/java/cn/hutool/setting/GroupedSet.java b/hutool-setting/src/main/java/cn/hutool/setting/GroupedSet.java new file mode 100644 index 000000000..6703e8830 --- /dev/null +++ b/hutool-setting/src/main/java/cn/hutool/setting/GroupedSet.java @@ -0,0 +1,315 @@ +package cn.hutool.setting; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; + +/** + * 分组化的Set集合类
+ * 在配置文件中可以用中括号分隔不同的分组,每个分组会放在独立的Set中,用group区别
+ * 无分组的集合和`[]`分组集合会合并成员,重名的分组也会合并成员
+ * 分组配置文件如下: + * + *
+ * [group1]
+ * aaa
+ * bbb
+ * ccc
+ * 
+ * [group2]
+ * aaa
+ * ccc
+ * ddd
+ * 
+ * + * @author Looly + * @since 3.1.0 + */ +public class GroupedSet extends HashMap> { + private static final long serialVersionUID = -8430706353275835496L; + // private final static Log log = StaticLog.get(); + + /** 注释符号(当有此符号在行首,表示此行为注释) */ + private final static String COMMENT_FLAG_PRE = "#"; + /** 分组行识别的环绕标记 */ + private final static char[] GROUP_SURROUND = { '[', ']' }; + + /** 本设置对象的字符集 */ + private Charset charset; + /** 设定文件的URL */ + private URL groupedSetUrl; + + /** + * 基本构造
+ * 需自定义初始化配置文件 + * + * @param charset 字符集 + */ + public GroupedSet(Charset charset) { + this.charset = charset; + } + + /** + * 构造,使用相对于Class文件根目录的相对路径 + * + * @param pathBaseClassLoader 相对路径(相对于当前项目的classes路径) + * @param charset 字符集 + */ + public GroupedSet(String pathBaseClassLoader, Charset charset) { + if (null == pathBaseClassLoader) { + pathBaseClassLoader = StrUtil.EMPTY; + } + + final URL url = URLUtil.getURL(pathBaseClassLoader); + if (url == null) { + throw new RuntimeException(StrUtil.format("Can not find GroupSet file: [{}]", pathBaseClassLoader)); + } + this.init(url, charset); + } + + /** + * 构造 + * + * @param configFile 配置文件对象 + * @param charset 字符集 + */ + public GroupedSet(File configFile, Charset charset) { + if (configFile == null) { + throw new RuntimeException("Null GroupSet file!"); + } + final URL url = URLUtil.getURL(configFile); + if (url == null) { + throw new RuntimeException(StrUtil.format("Can not find GroupSet file: [{}]", configFile.getAbsolutePath())); + } + this.init(url, charset); + } + + /** + * 构造,相对于classes读取文件 + * + * @param path 相对路径 + * @param clazz 基准类 + * @param charset 字符集 + */ + public GroupedSet(String path, Class clazz, Charset charset) { + final URL url = URLUtil.getURL(path, clazz); + if (url == null) { + throw new RuntimeException(StrUtil.format("Can not find GroupSet file: [{}]", path)); + } + this.init(url, charset); + } + + /** + * 构造 + * + * @param url 设定文件的URL + * @param charset 字符集 + */ + public GroupedSet(URL url, Charset charset) { + if (url == null) { + throw new RuntimeException("Null url define!"); + } + this.init(url, charset); + } + + /** + * 构造 + * + * @param pathBaseClassLoader 相对路径(相对于当前项目的classes路径) + */ + public GroupedSet(String pathBaseClassLoader) { + this(pathBaseClassLoader, CharsetUtil.CHARSET_UTF_8); + } + + /*--------------------------公有方法 start-------------------------------*/ + /** + * 初始化设定文件 + * + * @param groupedSetUrl 设定文件的URL + * @param charset 字符集 + * @return 成功初始化与否 + */ + public boolean init(URL groupedSetUrl, Charset charset) { + if (groupedSetUrl == null) { + throw new RuntimeException("Null GroupSet url or charset define!"); + } + this.charset = charset; + this.groupedSetUrl = groupedSetUrl; + + return this.load(groupedSetUrl); + } + + /** + * 加载设置文件 + * + * @param groupedSetUrl 配置文件URL + * @return 加载是否成功 + */ + synchronized public boolean load(URL groupedSetUrl) { + if (groupedSetUrl == null) { + throw new RuntimeException("Null GroupSet url define!"); + } + // log.debug("Load GroupSet file [{}]", groupedSetUrl.getPath()); + InputStream settingStream = null; + try { + settingStream = groupedSetUrl.openStream(); + load(settingStream); + } catch (IOException e) { + // log.error(e, "Load GroupSet error!"); + return false; + } finally { + IoUtil.close(settingStream); + } + return true; + } + + /** + * 重新加载配置文件 + */ + public void reload() { + this.load(groupedSetUrl); + } + + /** + * 加载设置文件。 此方法不会关闭流对象 + * + * @param settingStream 文件流 + * @return 加载成功与否 + * @throws IOException IO异常 + */ + public boolean load(InputStream settingStream) throws IOException { + super.clear(); + BufferedReader reader = null; + try { + reader = IoUtil.getReader(settingStream, charset); + // 分组 + String group = null; + LinkedHashSet valueSet = null; + + while (true) { + String line = reader.readLine(); + if (line == null) { + break; + } + line = line.trim(); + // 跳过注释行和空行 + if (StrUtil.isBlank(line) || line.startsWith(COMMENT_FLAG_PRE)) { + // 空行和注释忽略 + continue; + } else if (line.startsWith(StrUtil.BACKSLASH + COMMENT_FLAG_PRE)) { + // 对于值中出现开头为#的字符串,需要转义处理,在此做反转义 + line = line.substring(1); + } + + // 记录分组名 + if (line.charAt(0) == GROUP_SURROUND[0] && line.charAt(line.length() - 1) == GROUP_SURROUND[1]) { + // 开始新的分组取值,当出现重名分组时候,合并分组值 + group = line.substring(1, line.length() - 1).trim(); + valueSet = super.get(group); + if (null == valueSet) { + valueSet = new LinkedHashSet(); + } + super.put(group, valueSet); + continue; + } + + // 添加值 + if (null == valueSet) { + // 当出现无分组值的时候,会导致valueSet为空,此时group为"" + valueSet = new LinkedHashSet(); + super.put(StrUtil.EMPTY, valueSet); + } + valueSet.add(line); + } + } finally { + IoUtil.close(reader); + } + return true; + } + + /** + * @return 获得设定文件的路径 + */ + public String getPath() { + return groupedSetUrl.getPath(); + } + + /** + * @return 获得所有分组名 + */ + public Set getGroups() { + return super.keySet(); + } + + /** + * 获得对应分组的所有值 + * + * @param group 分组名 + * @return 分组的值集合 + */ + public LinkedHashSet getValues(String group) { + if (group == null) { + group = StrUtil.EMPTY; + } + return super.get(group); + } + + /** + * 是否在给定分组的集合中包含指定值
+ * 如果给定分组对应集合不存在,则返回false + * + * @param group 分组名 + * @param value 测试的值 + * @param otherValues 其他值 + * @return 是否包含 + */ + public boolean contains(String group, String value, String... otherValues) { + if (ArrayUtil.isNotEmpty(otherValues)) { + // 需要测试多个值的情况 + final List valueList = Arrays.asList(otherValues); + valueList.add(value); + return contains(group, valueList); + } else { + // 测试单个值 + final LinkedHashSet valueSet = getValues(group); + if (CollectionUtil.isEmpty(valueSet)) { + return false; + } + + return valueSet.contains(value); + } + } + + /** + * 是否在给定分组的集合中全部包含指定值集合
+ * 如果给定分组对应集合不存在,则返回false + * + * @param group 分组名 + * @param values 测试的值集合 + * @return 是否包含 + */ + public boolean contains(String group, Collection values) { + final LinkedHashSet valueSet = getValues(group); + if (CollectionUtil.isEmpty(values) || CollectionUtil.isEmpty(valueSet)) { + return false; + } + + return valueSet.containsAll(values); + } +} diff --git a/hutool-setting/src/main/java/cn/hutool/setting/Setting.java b/hutool-setting/src/main/java/cn/hutool/setting/Setting.java new file mode 100644 index 000000000..217a9fa7f --- /dev/null +++ b/hutool-setting/src/main/java/cn/hutool/setting/Setting.java @@ -0,0 +1,693 @@ +package cn.hutool.setting; + +import java.io.File; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.nio.file.WatchEvent; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.io.resource.ClassPathResource; +import cn.hutool.core.io.resource.FileResource; +import cn.hutool.core.io.resource.Resource; +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.io.resource.UrlResource; +import cn.hutool.core.io.watch.SimpleWatcher; +import cn.hutool.core.io.watch.WatchMonitor; +import cn.hutool.core.io.watch.WatchUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.log.StaticLog; +import cn.hutool.setting.dialect.Props; + +/** + * 设置工具类。 用于支持设置(配置)文件
+ * BasicSetting用于替换Properties类,提供功能更加强大的配置文件,同时对Properties文件向下兼容 + * + *
+ *  1、支持变量,默认变量命名为 ${变量名},变量只能识别读入行的变量,例如第6行的变量在第三行无法读取
+ *  2、支持分组,分组为中括号括起来的内容,中括号以下的行都为此分组的内容,无分组相当于空字符分组,若某个key是name,加上分组后的键相当于group.name
+ *  3、注释以#开头,但是空行和不带“=”的行也会被跳过,但是建议加#
+ *  4、store方法不会保存注释内容,慎重使用
+ * 
+ * + * @author looly + * + */ +public class Setting extends AbsSetting implements Map { + private static final long serialVersionUID = 3618305164959883393L; + + /** 默认字符集 */ + public final static Charset DEFAULT_CHARSET = CharsetUtil.CHARSET_UTF_8; + /** 默认配置文件扩展名 */ + public final static String EXT_NAME = "setting"; + + /** 附带分组的键值对存储 */ + private final GroupedMap groupedMap = new GroupedMap(); + + /** 本设置对象的字符集 */ + protected Charset charset; + /** 是否使用变量 */ + protected boolean isUseVariable; + /** 设定文件的URL */ + protected URL settingUrl; + + private SettingLoader settingLoader; + private WatchMonitor watchMonitor; + + // ------------------------------------------------------------------------------------- Constructor start + /** + * 空构造 + */ + public Setting() { + this.charset = DEFAULT_CHARSET; + } + + /** + * 构造 + * + * @param path 相对路径或绝对路径 + */ + public Setting(String path) { + this(path, false); + } + + /** + * 构造 + * + * @param path 相对路径或绝对路径 + * @param isUseVariable 是否使用变量 + */ + public Setting(String path, boolean isUseVariable) { + this(path, DEFAULT_CHARSET, isUseVariable); + } + + /** + * 构造,使用相对于Class文件根目录的相对路径 + * + * @param path 相对路径或绝对路径 + * @param charset 字符集 + * @param isUseVariable 是否使用变量 + */ + public Setting(String path, Charset charset, boolean isUseVariable) { + Assert.notBlank(path, "Blank setting path !"); + this.init(ResourceUtil.getResourceObj(path), charset, isUseVariable); + } + + /** + * 构造 + * + * @param configFile 配置文件对象 + * @param charset 字符集 + * @param isUseVariable 是否使用变量 + */ + public Setting(File configFile, Charset charset, boolean isUseVariable) { + Assert.notNull(configFile, "Null setting file define!"); + this.init(new FileResource(configFile), charset, isUseVariable); + } + + /** + * 构造,相对于classes读取文件 + * + * @param path 相对ClassPath路径或绝对路径 + * @param clazz 基准类 + * @param charset 字符集 + * @param isUseVariable 是否使用变量 + */ + public Setting(String path, Class clazz, Charset charset, boolean isUseVariable) { + Assert.notBlank(path, "Blank setting path !"); + this.init(new ClassPathResource(path, clazz), charset, isUseVariable); + } + + /** + * 构造 + * + * @param url 设定文件的URL + * @param charset 字符集 + * @param isUseVariable 是否使用变量 + */ + public Setting(URL url, Charset charset, boolean isUseVariable) { + Assert.notNull(url, "Null setting url define!"); + this.init(new UrlResource(url), charset, isUseVariable); + } + // ------------------------------------------------------------------------------------- Constructor end + + /** + * 初始化设定文件 + * + * @param resource {@link Resource} + * @param charset 字符集 + * @param isUseVariable 是否使用变量 + * @return 成功初始化与否 + */ + public boolean init(Resource resource, Charset charset, boolean isUseVariable) { + if (resource == null) { + throw new NullPointerException("Null setting url define!"); + } + this.settingUrl = resource.getUrl(); + this.charset = charset; + this.isUseVariable = isUseVariable; + + return load(); + } + + /** + * 重新加载配置文件 + * + * @return 是否加载成功 + */ + synchronized public boolean load() { + if (null == this.settingLoader) { + settingLoader = new SettingLoader(this.groupedMap, this.charset, this.isUseVariable); + } + return settingLoader.load(new UrlResource(this.settingUrl)); + } + + /** + * 在配置文件变更时自动加载 + * + * @param autoReload 是否自动加载 + */ + public void autoLoad(boolean autoReload) { + if (autoReload) { + Assert.notNull(this.settingUrl, "Setting URL is null !"); + if (null != this.watchMonitor) { + // 先关闭之前的监听 + this.watchMonitor.close(); + } + this.watchMonitor = WatchUtil.createModify(this.settingUrl, new SimpleWatcher() { + @Override + public void onModify(WatchEvent event, Path currentPath) { + load(); + } + }); + this.watchMonitor.start(); + StaticLog.debug("Auto load for [{}] listenning...", this.settingUrl); + } else { + IoUtil.close(this.watchMonitor); + this.watchMonitor = null; + } + } + + /** + * @return 获得设定文件的路径 + */ + public String getSettingPath() { + return (null == this.settingUrl) ? null : this.settingUrl.getPath(); + } + + /** + * 键值总数 + * + * @return 键值总数 + */ + public int size() { + return this.groupedMap.size(); + } + + @Override + public String getByGroup(String key, String group) { + return this.groupedMap.get(group, key); + } + + /** + * 获取并删除键值对,当指定键对应值非空时,返回并删除这个值,后边的键对应的值不再查找 + * + * @param keys 键列表,常用于别名 + * @return 值 + * @since 3.1.2 + */ + public Object getAndRemove(String... keys) { + Object value = null; + for (String key : keys) { + value = remove(key); + if (null != value) { + break; + } + } + return value; + } + + /** + * 获取并删除键值对,当指定键对应值非空时,返回并删除这个值,后边的键对应的值不再查找 + * + * @param keys 键列表,常用于别名 + * @return 字符串值 + * @since 3.1.2 + */ + public String getAndRemoveStr(String... keys) { + Object value = null; + for (String key : keys) { + value = remove(key); + if (null != value) { + break; + } + } + return (String) value; + } + + /** + * 获得指定分组的所有键值对,此方法获取的是原始键值对,获取的键值对可以被修改 + * + * @param group 分组 + * @return map + */ + public Map getMap(String group) { + final LinkedHashMap map = this.groupedMap.get(group); + return (null != map) ? map : new LinkedHashMap(0); + } + + /** + * 获取group分组下所有配置键值对,组成新的{@link Setting} + * + * @param group 分组 + * @return {@link Setting} + */ + public Setting getSetting(String group) { + final Setting setting = new Setting(); + setting.putAll(this.getMap(group)); + return setting; + } + + /** + * 获取group分组下所有配置键值对,组成新的{@link Properties} + * + * @param group 分组 + * @return Properties对象 + */ + public Properties getProperties(String group) { + final Properties properties = new Properties(); + properties.putAll(getMap(group)); + return properties; + } + + /** + * 获取group分组下所有配置键值对,组成新的{@link Props} + * + * @param group 分组 + * @return Props对象 + * @since 4.1.21 + */ + public Props getProps(String group) { + final Props props = new Props(); + props.putAll(getMap(group)); + return props; + } + + // --------------------------------------------------------------------------------- Functions + /** + * 持久化当前设置,会覆盖掉之前的设置
+ * 持久化不会保留之前的分组 + * + * @param absolutePath 设置文件的绝对路径 + */ + public void store(String absolutePath) { + if (null == this.settingLoader) { + settingLoader = new SettingLoader(this.groupedMap, this.charset, this.isUseVariable); + } + settingLoader.store(absolutePath); + } + + /** + * 转换为Properties对象,原分组变为前缀 + * + * @return Properties对象 + */ + public Properties toProperties() { + final Properties properties = new Properties(); + String group; + for (Entry> groupEntry : this.groupedMap.entrySet()) { + group = groupEntry.getKey(); + for (Entry entry : groupEntry.getValue().entrySet()) { + properties.setProperty(StrUtil.isEmpty(group) ? entry.getKey() : group + CharUtil.DOT + entry.getKey(), entry.getValue()); + } + } + return properties; + } + + /** + * 获取GroupedMap + * + * @return GroupedMap + * @since 4.0.12 + */ + public GroupedMap getGroupedMap() { + return this.groupedMap; + } + + /** + * 获取所有分组 + * + * @return 获得所有分组名 + */ + public List getGroups() { + return CollUtil.newArrayList(this.groupedMap.keySet()); + } + + /** + * 设置变量的正则
+ * 正则只能有一个group表示变量本身,剩余为字符 例如 \$\{(name)\}表示${name}变量名为name的一个变量表示 + * + * @param regex 正则 + */ + public Setting setVarRegex(String regex) { + if (null == this.settingLoader) { + throw new NullPointerException("SettingLoader is null !"); + } + this.settingLoader.setVarRegex(regex); + return this; + } + + /** + * 自定义字符编码 + * + * @param charset 字符编码 + * @return this + * @since 4.6.2 + */ + public Setting setCharset(Charset charset) { + this.charset = charset; + return this; + } + + // ------------------------------------------------- Map interface with group + /** + * 某个分组对应的键值对是否为空 + * + * @param group 分组 + * @return 是否为空 + */ + public boolean isEmpty(String group) { + return this.groupedMap.isEmpty(group); + } + + /** + * 指定分组中是否包含指定key + * + * @param group 分组 + * @param key 键 + * @return 是否包含key + */ + public boolean containsKey(String group, String key) { + return this.groupedMap.containsKey(group, key); + } + + /** + * 指定分组中是否包含指定值 + * + * @param group 分组 + * @param value 值 + * @return 是否包含值 + */ + public boolean containsValue(String group, String value) { + return this.groupedMap.containsValue(group, value); + } + + /** + * 获取分组对应的值,如果分组不存在或者值不存在则返回null + * + * @param group 分组 + * @param key 键 + * @return 值,如果分组不存在或者值不存在则返回null + */ + public String get(String group, String key) { + return this.groupedMap.get(group, key); + } + + /** + * 将键值对加入到对应分组中 + * + * @param group 分组 + * @param key 键 + * @param value 值 + * @return 此key之前存在的值,如果没有返回null + */ + public String put(String group, String key, String value) { + return this.groupedMap.put(group, key, value); + } + + /** + * 从指定分组中删除指定值 + * + * @param group 分组 + * @param key 键 + * @return 被删除的值,如果值不存在,返回null + */ + public String remove(String group, Object key) { + return this.groupedMap.remove(group, Convert.toStr(key)); + } + + /** + * 加入多个键值对到某个分组下 + * + * @param group 分组 + * @param m 键值对 + * @return this + */ + public Setting putAll(String group, Map m) { + this.groupedMap.putAll(group, m); + return this; + } + + /** + * 清除指定分组下的所有键值对 + * + * @param group 分组 + * @return this + */ + public Setting clear(String group) { + this.groupedMap.clear(group); + return this; + } + + /** + * 指定分组所有键的Set + * + * @param group 分组 + * @return 键Set + */ + public Set keySet(String group) { + return this.groupedMap.keySet(group); + } + + /** + * 指定分组下所有值 + * + * @param group 分组 + * @return 值 + */ + public Collection values(String group) { + return this.groupedMap.values(group); + } + + /** + * 指定分组下所有键值对 + * + * @param group 分组 + * @return 键值对 + */ + public Set> entrySet(String group) { + return this.groupedMap.entrySet(group); + } + + /** + * 设置值 + * + * @param key 键 + * @param value 值 + * @return this + * @since 3.3.1 + */ + public Setting set(String key, String value) { + this.put(key, value); + return this; + } + + /** + * 将键值对加入到对应分组中 + * + * @param group 分组 + * @param key 键 + * @param value 值 + * @return 此key之前存在的值,如果没有返回null + */ + public Setting set(String group, String key, String value) { + this.put(group, key, value); + return this; + } + + // ------------------------------------------------- Override Map interface + @Override + public boolean isEmpty() { + return this.groupedMap.isEmpty(); + } + + /** + * 默认分组(空分组)中是否包含指定key对应的值 + * + * @param key 键 + * @return 默认分组中是否包含指定key对应的值 + */ + @Override + public boolean containsKey(Object key) { + return this.groupedMap.containsKey(DEFAULT_GROUP, Convert.toStr(key)); + } + + /** + * 默认分组(空分组)中是否包含指定值 + * + * @param value 值 + * @return 默认分组中是否包含指定值 + */ + @Override + public boolean containsValue(Object value) { + return this.groupedMap.containsValue(DEFAULT_GROUP, Convert.toStr(value)); + } + + /** + * 获取默认分组(空分组)中指定key对应的值 + * + * @param key 键 + * @return 默认分组(空分组)中指定key对应的值 + */ + @Override + public String get(Object key) { + return this.groupedMap.get(DEFAULT_GROUP, Convert.toStr(key)); + } + + /** + * 将指定键值对加入到默认分组(空分组)中 + * + * @param key 键 + * @param value 值 + * @return 加入的值 + */ + @Override + public String put(String key, String value) { + return this.groupedMap.put(DEFAULT_GROUP, key, value); + } + + /** + * 移除默认分组(空分组)中指定值 + * + * @param key 键 + * @return 移除的值 + */ + @Override + public String remove(Object key) { + return this.groupedMap.remove(DEFAULT_GROUP, Convert.toStr(key)); + } + + /** + * 将键值对Map加入默认分组(空分组)中 + * + * @param m Map + */ + @Override + public void putAll(Map m) { + this.groupedMap.putAll(DEFAULT_GROUP, m); + } + + /** + * 清空默认分组(空分组)中的所有键值对 + */ + @Override + public void clear() { + this.groupedMap.clear(DEFAULT_GROUP); + } + + /** + * 获取默认分组(空分组)中的所有键列表 + * + * @return 默认分组(空分组)中的所有键列表 + */ + @Override + public Set keySet() { + return this.groupedMap.keySet(DEFAULT_GROUP); + } + + /** + * 获取默认分组(空分组)中的所有值列表 + * + * @return 默认分组(空分组)中的所有值列表 + */ + @Override + public Collection values() { + return this.groupedMap.values(DEFAULT_GROUP); + } + + /** + * 获取默认分组(空分组)中的所有键值对列表 + * + * @return 默认分组(空分组)中的所有键值对列表 + */ + @Override + public Set> entrySet() { + return this.groupedMap.entrySet(DEFAULT_GROUP); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((charset == null) ? 0 : charset.hashCode()); + result = prime * result + ((groupedMap == null) ? 0 : groupedMap.hashCode()); + result = prime * result + (isUseVariable ? 1231 : 1237); + result = prime * result + ((settingUrl == null) ? 0 : settingUrl.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; + } + Setting other = (Setting) obj; + if (charset == null) { + if (other.charset != null) { + return false; + } + } else if (false == charset.equals(other.charset)) { + return false; + } + if (groupedMap == null) { + if (other.groupedMap != null) { + return false; + } + } else if (false == groupedMap.equals(other.groupedMap)) { + return false; + } + if (isUseVariable != other.isUseVariable) { + return false; + } + if (settingUrl == null) { + if (other.settingUrl != null) { + return false; + } + } else if (!settingUrl.equals(other.settingUrl)) { + return false; + } + return true; + } + + @Override + public String toString() { + return groupedMap.toString(); + } +} diff --git a/hutool-setting/src/main/java/cn/hutool/setting/SettingLoader.java b/hutool-setting/src/main/java/cn/hutool/setting/SettingLoader.java new file mode 100644 index 000000000..c416836a6 --- /dev/null +++ b/hutool-setting/src/main/java/cn/hutool/setting/SettingLoader.java @@ -0,0 +1,225 @@ +package cn.hutool.setting; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.nio.charset.Charset; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map.Entry; +import java.util.Set; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.io.resource.UrlResource; +import cn.hutool.core.util.CharUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.ReUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; + +/** + * Setting文件加载器 + * + * @author Looly + * + */ +public class SettingLoader { + private static Log log = LogFactory.get(); + + /** 注释符号(当有此符号在行首,表示此行为注释) */ + private final static char COMMENT_FLAG_PRE = '#'; + /** 赋值分隔符(用于分隔键值对) */ + private final static char ASSIGN_FLAG = '='; + /** 变量名称的正则 */ + private String reg_var = "\\$\\{(.*?)\\}"; + + /** 本设置对象的字符集 */ + private Charset charset; + /** 是否使用变量 */ + private boolean isUseVariable; + /** GroupedMap */ + private GroupedMap groupedMap; + + /** + * 构造 + * + * @param groupedMap GroupedMap + */ + public SettingLoader(GroupedMap groupedMap) { + this(groupedMap, CharsetUtil.CHARSET_UTF_8, false); + } + + /** + * 构造 + * + * @param groupedMap GroupedMap + * @param charset 编码 + * @param isUseVariable 是否使用变量 + */ + public SettingLoader(GroupedMap groupedMap, Charset charset, boolean isUseVariable) { + this.groupedMap = groupedMap; + this.charset = charset; + this.isUseVariable = isUseVariable; + } + + /** + * 加载设置文件 + * + * @param urlResource 配置文件URL + * @return 加载是否成功 + */ + public boolean load(UrlResource urlResource) { + if (urlResource == null) { + throw new NullPointerException("Null setting url define!"); + } + log.debug("Load setting file [{}]", urlResource); + InputStream settingStream = null; + try { + settingStream = urlResource.getStream(); + load(settingStream); + } catch (Exception e) { + log.error(e, "Load setting error!"); + return false; + } finally { + IoUtil.close(settingStream); + } + return true; + } + + /** + * 加载设置文件。 此方法不会关闭流对象 + * + * @param settingStream 文件流 + * @return 加载成功与否 + * @throws IOException IO异常 + */ + synchronized public boolean load(InputStream settingStream) throws IOException { + this.groupedMap.clear(); + BufferedReader reader = null; + try { + reader = IoUtil.getReader(settingStream, this.charset); + // 分组 + String group = null; + + String line; + while (true) { + line = reader.readLine(); + if (line == null) { + break; + } + line = line.trim(); + // 跳过注释行和空行 + if (StrUtil.isBlank(line) || StrUtil.startWith(line, COMMENT_FLAG_PRE)) { + continue; + } + + // 记录分组名 + if (StrUtil.isSurround(line, CharUtil.BRACKET_START, CharUtil.BRACKET_END)) { + group = line.substring(1, line.length() - 1).trim(); + continue; + } + + final String[] keyValue = StrUtil.splitToArray(line, ASSIGN_FLAG, 2); + // 跳过不符合键值规范的行 + if (keyValue.length < 2) { + continue; + } + + String value = keyValue[1].trim(); + // 替换值中的所有变量变量(变量必须是此行之前定义的变量,否则无法找到) + if (this.isUseVariable) { + value = replaceVar(group, value); + } + this.groupedMap.put(group, keyValue[0].trim(), value); + } + } finally { + IoUtil.close(reader); + } + return true; + } + + /** + * 设置变量的正则
+ * 正则只能有一个group表示变量本身,剩余为字符 例如 \$\{(name)\}表示${name}变量名为name的一个变量表示 + * + * @param regex 正则 + */ + public void setVarRegex(String regex) { + this.reg_var = regex; + } + + /** + * 持久化当前设置,会覆盖掉之前的设置
+ * 持久化会不会保留之前的分组 + * + * @param absolutePath 设置文件的绝对路径 + */ + public void store(String absolutePath) { + PrintWriter writer = null; + try { + writer = FileUtil.getPrintWriter(absolutePath, charset, false); + store(writer); + } catch (IOException e) { + throw new IORuntimeException(e, "Store Setting to [{}] error!", absolutePath); + } finally { + IoUtil.close(writer); + } + } + + /** + * 存储到Writer + * + * @param writer Writer + * @throws IOException IO异常 + */ + synchronized private void store(PrintWriter writer) throws IOException { + for (Entry> groupEntry : this.groupedMap.entrySet()) { + writer.println(StrUtil.format("{}{}{}", CharUtil.BRACKET_START, groupEntry.getKey(), CharUtil.BRACKET_END)); + for (Entry entry : groupEntry.getValue().entrySet()) { + writer.println(StrUtil.format("{} {} {}", entry.getKey(), ASSIGN_FLAG, entry.getValue())); + } + } + } + + // ----------------------------------------------------------------------------------- Private method start + /** + * 替换给定值中的变量标识 + * + * @param group 所在分组 + * @param value 值 + * @return 替换后的字符串 + */ + private String replaceVar(String group, String value) { + // 找到所有变量标识 + final Set vars = ReUtil.findAll(reg_var, value, 0, new HashSet()); + String key; + for (String var : vars) { + key = ReUtil.get(reg_var, var, 1); + if (StrUtil.isNotBlank(key)) { + // 查找变量名对应的值 + String varValue = this.groupedMap.get(group, key); + if (null != varValue) { + // 替换标识 + value = value.replace(var, varValue); + } else { + // 跨分组查找 + final List groupAndKey = StrUtil.split(key, CharUtil.DOT, 2); + if (groupAndKey.size() > 1) { + varValue = this.groupedMap.get(groupAndKey.get(0), groupAndKey.get(1)); + if (null != varValue) { + // 替换标识 + value = value.replace(var, varValue); + } + } + } + } + } + return value; + } + // ----------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-setting/src/main/java/cn/hutool/setting/SettingRuntimeException.java b/hutool-setting/src/main/java/cn/hutool/setting/SettingRuntimeException.java new file mode 100644 index 000000000..168efa12c --- /dev/null +++ b/hutool-setting/src/main/java/cn/hutool/setting/SettingRuntimeException.java @@ -0,0 +1,31 @@ +package cn.hutool.setting; + +import cn.hutool.core.util.StrUtil; + +/** + * 设置异常 + * @author xiaoleilu + */ +public class SettingRuntimeException extends RuntimeException{ + private static final long serialVersionUID = 7941096116780378387L; + + public SettingRuntimeException(Throwable e) { + super(e); + } + + public SettingRuntimeException(String message) { + super(message); + } + + public SettingRuntimeException(String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params)); + } + + public SettingRuntimeException(String message, Throwable throwable) { + super(message, throwable); + } + + public SettingRuntimeException(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } +} diff --git a/hutool-setting/src/main/java/cn/hutool/setting/SettingUtil.java b/hutool-setting/src/main/java/cn/hutool/setting/SettingUtil.java new file mode 100644 index 000000000..3a5ff1a94 --- /dev/null +++ b/hutool-setting/src/main/java/cn/hutool/setting/SettingUtil.java @@ -0,0 +1,46 @@ +package cn.hutool.setting; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.StrUtil; + +/** + * Setting工具类
+ * 提供静态方法获取配置文件 + * + * @author looly + * + */ +public class SettingUtil { + /** 配置文件缓存 */ + private static Map settingMap = new ConcurrentHashMap<>(); + private static Object lock = new Object(); + + /** + * 获取当前环境下的配置文件
+ * name可以为不包括扩展名的文件名(默认.setting为结尾),也可以是文件名全称 + * + * @param name 文件名,如果没有扩展名,默认为.setting + * @return 当前环境下配置文件 + */ + public static Setting get(String name) { + Setting setting = settingMap.get(name); + if (null == setting) { + synchronized (lock) { + setting = settingMap.get(name); + if (null == setting) { + String filePath = name; + String extName = FileUtil.extName(filePath); + if(StrUtil.isEmpty(extName)) { + filePath = filePath + "." + Setting.EXT_NAME; + } + setting = new Setting(filePath, true); + settingMap.put(name, setting); + } + } + } + return setting; + } +} diff --git a/hutool-setting/src/main/java/cn/hutool/setting/dialect/Props.java b/hutool-setting/src/main/java/cn/hutool/setting/dialect/Props.java new file mode 100644 index 000000000..d2705dc38 --- /dev/null +++ b/hutool-setting/src/main/java/cn/hutool/setting/dialect/Props.java @@ -0,0 +1,514 @@ +package cn.hutool.setting.dialect; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.Writer; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.WatchEvent; +import java.util.Date; +import java.util.Properties; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.getter.BasicTypeGetter; +import cn.hutool.core.getter.OptBasicTypeGetter; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.io.resource.ClassPathResource; +import cn.hutool.core.io.resource.FileResource; +import cn.hutool.core.io.resource.Resource; +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.io.resource.UrlResource; +import cn.hutool.core.io.watch.SimpleWatcher; +import cn.hutool.core.io.watch.WatchMonitor; +import cn.hutool.core.io.watch.WatchUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import cn.hutool.setting.SettingRuntimeException; + +/** + * Properties文件读取封装类 + * + * @author loolly + */ +public final class Props extends Properties implements BasicTypeGetter, OptBasicTypeGetter { + private static final long serialVersionUID = 1935981579709590740L; + private final static Log log = LogFactory.get(); + + // ----------------------------------------------------------------------- 私有属性 start + /** 属性文件的URL */ + private URL propertiesFileUrl; + private WatchMonitor watchMonitor; + /** properties文件编码 */ + private Charset charset = CharsetUtil.CHARSET_ISO_8859_1; + // ----------------------------------------------------------------------- 私有属性 end + + /** + * 获得Classpath下的Properties文件 + * + * @param resource 资源(相对Classpath的路径) + * @return Properties + */ + public static Properties getProp(String resource) { + return new Props(resource); + } + + /** + * 获得Classpath下的Properties文件 + * + * @param resource 资源(相对Classpath的路径) + * @param charsetName 字符集 + * @return Properties + */ + public static Properties getProp(String resource, String charsetName) { + return new Props(resource, charsetName); + } + + /** + * 获得Classpath下的Properties文件 + * + * @param resource 资源(相对Classpath的路径) + * @param charset 字符集 + * @return Properties + */ + public static Properties getProp(String resource, Charset charset) { + return new Props(resource, charset); + } + + // ----------------------------------------------------------------------- 构造方法 start + /** + * 构造 + */ + public Props() { + super(); + } + + /** + * 构造,使用相对于Class文件根目录的相对路径 + * + * @param path + */ + public Props(String path) { + this(path, CharsetUtil.CHARSET_ISO_8859_1); + } + + /** + * 构造,使用相对于Class文件根目录的相对路径 + * + * @param path 相对或绝对路径 + * @param charsetName 字符集 + */ + public Props(String path, String charsetName) { + this(path, CharsetUtil.charset(charsetName)); + } + + /** + * 构造,使用相对于Class文件根目录的相对路径 + * + * @param path 相对或绝对路径 + * @param charset 字符集 + */ + public Props(String path, Charset charset) { + Assert.notBlank(path, "Blank properties file path !"); + if(null != charset) { + this.charset = charset; + } + this.load(ResourceUtil.getResourceObj(path)); + } + + /** + * 构造 + * + * @param propertiesFile 配置文件对象 + */ + public Props(File propertiesFile) { + this(propertiesFile, StandardCharsets.ISO_8859_1); + } + + /** + * 构造 + * + * @param propertiesFile 配置文件对象 + * @param charsetName 字符集 + */ + public Props(File propertiesFile, String charsetName) { + this(propertiesFile, Charset.forName(charsetName)); + } + + /** + * 构造 + * + * @param propertiesFile 配置文件对象 + * @param charset 字符集 + */ + public Props(File propertiesFile, Charset charset) { + Assert.notNull(propertiesFile, "Null properties file!"); + this.charset = charset; + this.load(new FileResource(propertiesFile)); + } + + /** + * 构造,相对于classes读取文件 + * + * @param path 相对路径 + * @param clazz 基准类 + */ + public Props(String path, Class clazz) { + this(path, clazz, CharsetUtil.ISO_8859_1); + } + + /** + * 构造,相对于classes读取文件 + * + * @param path 相对路径 + * @param clazz 基准类 + * @param charsetName 字符集 + */ + public Props(String path, Class clazz, String charsetName) { + this(path, clazz, CharsetUtil.charset(charsetName)); + } + + /** + * 构造,相对于classes读取文件 + * + * @param path 相对路径 + * @param clazz 基准类 + * @param charset 字符集 + */ + public Props(String path, Class clazz, Charset charset) { + Assert.notBlank(path, "Blank properties file path !"); + if(null != charset) { + this.charset = charset; + } + this.load(new ClassPathResource(path, clazz)); + } + + /** + * 构造,使用URL读取 + * + * @param propertiesUrl 属性文件路径 + */ + public Props(URL propertiesUrl) { + this(propertiesUrl, StandardCharsets.ISO_8859_1); + } + + /** + * 构造,使用URL读取 + * + * @param propertiesUrl 属性文件路径 + * @param charsetName 字符集 + */ + public Props(URL propertiesUrl, String charsetName) { + this(propertiesUrl, CharsetUtil.charset(charsetName)); + } + + /** + * 构造,使用URL读取 + * + * @param propertiesUrl 属性文件路径 + * @param charset 字符集 + */ + public Props(URL propertiesUrl, Charset charset) { + Assert.notNull(propertiesUrl, "Null properties URL !"); + if(null != charset) { + this.charset = charset; + } + this.load(new UrlResource(propertiesUrl)); + } + + /** + * 构造,使用URL读取 + * + * @param properties 属性文件路径 + */ + public Props(Properties properties) { + if (CollectionUtil.isNotEmpty(properties)) { + this.putAll(properties); + } + } + + // ----------------------------------------------------------------------- 构造方法 end + + /** + * 初始化配置文件 + * + * @param urlResource {@link UrlResource} + */ + public void load(Resource urlResource) { + this.propertiesFileUrl = urlResource.getUrl(); + if (null == this.propertiesFileUrl) { + throw new SettingRuntimeException("Can not find properties file: [{}]", urlResource); + } + log.debug("Load properties [{}]", propertiesFileUrl.getPath()); + try (final BufferedReader reader = urlResource.getReader(charset)) { + super.load(reader); + } catch (Exception e) { + log.error(e, "Load properties error!"); + } + } + + /** + * 重新加载配置文件 + */ + public void load() { + this.load(new UrlResource(this.propertiesFileUrl)); + } + + /** + * 在配置文件变更时自动加载 + * + * @param autoReload 是否自动加载 + */ + public void autoLoad(boolean autoReload) { + if (autoReload) { + Assert.notNull(this.propertiesFileUrl, "Properties URL is null !"); + if (null != this.watchMonitor) { + // 先关闭之前的监听 + this.watchMonitor.close(); + } + this.watchMonitor = WatchUtil.createModify(this.propertiesFileUrl, new SimpleWatcher(){ + @Override + public void onModify(WatchEvent event, Path currentPath) { + load(); + } + }); + this.watchMonitor.start(); + } else { + IoUtil.close(this.watchMonitor); + this.watchMonitor = null; + } + } + + // ----------------------------------------------------------------------- Get start + @Override + public Object getObj(String key, Object defaultValue) { + return getStr(key, null == defaultValue ? null : defaultValue.toString()); + } + + @Override + public Object getObj(String key) { + return getObj(key, null); + } + + @Override + public String getStr(String key, String defaultValue) { + return super.getProperty(key, defaultValue); + } + + @Override + public String getStr(String key) { + return super.getProperty(key); + } + + @Override + public Integer getInt(String key, Integer defaultValue) { + return Convert.toInt(getStr(key), defaultValue); + } + + @Override + public Integer getInt(String key) { + return getInt(key, null); + } + + @Override + public Boolean getBool(String key, Boolean defaultValue) { + return Convert.toBool(getStr(key), defaultValue); + } + + @Override + public Boolean getBool(String key) { + return getBool(key, null); + } + + @Override + public Long getLong(String key, Long defaultValue) { + return Convert.toLong(getStr(key), defaultValue); + } + + @Override + public Long getLong(String key) { + return getLong(key, null); + } + + @Override + public Character getChar(String key, Character defaultValue) { + final String value = getStr(key); + if (StrUtil.isBlank(value)) { + return defaultValue; + } + return value.charAt(0); + } + + @Override + public Character getChar(String key) { + return getChar(key, null); + } + + @Override + public Float getFloat(String key) { + return getFloat(key, null); + } + + @Override + public Float getFloat(String key, Float defaultValue) { + return Convert.toFloat(getStr(key), defaultValue); + } + + @Override + public Double getDouble(String key, Double defaultValue) throws NumberFormatException { + return Convert.toDouble(getStr(key), defaultValue); + } + + @Override + public Double getDouble(String key) throws NumberFormatException { + return getDouble(key, null); + } + + @Override + public Short getShort(String key, Short defaultValue) { + return Convert.toShort(getStr(key), defaultValue); + } + + @Override + public Short getShort(String key) { + return getShort(key, null); + } + + @Override + public Byte getByte(String key, Byte defaultValue) { + return Convert.toByte(getStr(key), defaultValue); + } + + @Override + public Byte getByte(String key) { + return getByte(key, null); + } + + @Override + public BigDecimal getBigDecimal(String key, BigDecimal defaultValue) { + final String valueStr = getStr(key); + if (StrUtil.isBlank(valueStr)) { + return defaultValue; + } + + try { + return new BigDecimal(valueStr); + } catch (Exception e) { + return defaultValue; + } + } + + @Override + public BigDecimal getBigDecimal(String key) { + return getBigDecimal(key, null); + } + + @Override + public BigInteger getBigInteger(String key, BigInteger defaultValue) { + final String valueStr = getStr(key); + if (StrUtil.isBlank(valueStr)) { + return defaultValue; + } + + try { + return new BigInteger(valueStr); + } catch (Exception e) { + return defaultValue; + } + } + + @Override + public BigInteger getBigInteger(String key) { + return getBigInteger(key, null); + } + + @Override + public > E getEnum(Class clazz, String key, E defaultValue) { + return Convert.toEnum(clazz, getStr(key), defaultValue); + } + + @Override + public > E getEnum(Class clazz, String key) { + return getEnum(clazz, key, null); + } + + @Override + public Date getDate(String key, Date defaultValue) { + return Convert.toDate(getStr(key), defaultValue); + } + + @Override + public Date getDate(String key) { + return getDate(key, null); + } + + /** + * 获取并删除键值对,当指定键对应值非空时,返回并删除这个值,后边的键对应的值不再查找 + * + * @param keys 键列表,常用于别名 + * @return 字符串值 + * @since 4.1.21 + */ + public String getAndRemoveStr(String... keys) { + Object value = null; + for (String key : keys) { + value = remove(key); + if (null != value) { + break; + } + } + return (String) value; + } + + // ----------------------------------------------------------------------- Get end + + // ----------------------------------------------------------------------- Set start + /** + * 设置值,无给定键创建之。设置后未持久化 + * + * @param key 属性键 + * @param value 属性值 + */ + public void setProperty(String key, Object value) { + super.setProperty(key, value.toString()); + } + + /** + * 持久化当前设置,会覆盖掉之前的设置 + * + * @param absolutePath 设置文件的绝对路径 + * @throws IORuntimeException IO异常,可能为文件未找到 + */ + public void store(String absolutePath) throws IORuntimeException{ + Writer writer = null; + try { + writer = FileUtil.getWriter(absolutePath, charset, false); + super.store(writer, null); + } catch (IOException e) { + throw new IORuntimeException(e, "Store properties to [{}] error!", absolutePath); + } finally { + IoUtil.close(writer); + } + } + + /** + * 存储当前设置,会覆盖掉以前的设置 + * + * @param path 相对路径 + * @param clazz 相对的类 + */ + public void store(String path, Class clazz) { + this.store(FileUtil.getAbsolutePath(path, clazz)); + } + // ----------------------------------------------------------------------- Set end +} diff --git a/hutool-setting/src/main/java/cn/hutool/setting/dialect/package-info.java b/hutool-setting/src/main/java/cn/hutool/setting/dialect/package-info.java new file mode 100644 index 000000000..ea6c645e3 --- /dev/null +++ b/hutool-setting/src/main/java/cn/hutool/setting/dialect/package-info.java @@ -0,0 +1,7 @@ +/** + * 配置文件实现分装,例如Properties封装Props + * + * @author looly + * + */ +package cn.hutool.setting.dialect; \ No newline at end of file diff --git a/hutool-setting/src/main/java/cn/hutool/setting/package-info.java b/hutool-setting/src/main/java/cn/hutool/setting/package-info.java new file mode 100644 index 000000000..0120b3759 --- /dev/null +++ b/hutool-setting/src/main/java/cn/hutool/setting/package-info.java @@ -0,0 +1,7 @@ +/** + * Setting模块主要针对Properties文件读写做封装,同时定义一套自己的配置文件规范,实现兼容性良好的配置工具。 + * + * @author looly + * + */ +package cn.hutool.setting; \ No newline at end of file diff --git a/hutool-setting/src/main/java/cn/hutool/setting/profile/GlobalProfile.java b/hutool-setting/src/main/java/cn/hutool/setting/profile/GlobalProfile.java new file mode 100644 index 000000000..38a20d7dd --- /dev/null +++ b/hutool-setting/src/main/java/cn/hutool/setting/profile/GlobalProfile.java @@ -0,0 +1,36 @@ +package cn.hutool.setting.profile; + +import cn.hutool.core.lang.Singleton; +import cn.hutool.setting.Setting; + +/** + * 全局的Profile配置中心 + * + * @author Looly + * + */ +public class GlobalProfile { + + private GlobalProfile() { + } + + // -------------------------------------------------------------------------------- Static method start + /** + * 设置全局环境 + * @param profile 环境 + * @return {@link Profile} + */ + public static Profile setProfile(String profile) { + return Singleton.get(Profile.class, profile); + } + + /** + * 获得全局的当前环境下对应的配置文件 + * @param settingName 配置文件名,可以忽略默认后者(.setting) + * @return {@link Setting} + */ + public static Setting getSetting(String settingName) { + return Singleton.get(Profile.class).getSetting(settingName); + } + // -------------------------------------------------------------------------------- Static method end +} diff --git a/hutool-setting/src/main/java/cn/hutool/setting/profile/Profile.java b/hutool-setting/src/main/java/cn/hutool/setting/profile/Profile.java new file mode 100644 index 000000000..2505a26bc --- /dev/null +++ b/hutool-setting/src/main/java/cn/hutool/setting/profile/Profile.java @@ -0,0 +1,148 @@ +package cn.hutool.setting.profile; + +import java.io.Serializable; +import java.nio.charset.Charset; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.hutool.setting.Setting; + +/** + * Profile可以让我们定义一系列的配置信息,然后指定其激活条件。
+ * 此类中我们规范一套规则如下:
+ * 默认的,我们读取${classpath}/default下的配置文件(*.setting文件),当调用setProfile方法时,指定一个profile,即可读取其目录下的配置文件。
+ * 比如我们定义几个profile:test,develop,production,分别代表测试环境、开发环境和线上环境,我希望读取数据库配置文件db.setting,那么: + *
    + *
  1. test =》 ${classpath}/test/db.setting
  2. + *
  3. develop =》 ${classpath}/develop/db.setting
  4. + *
  5. production =》 ${classpath}/production/db.setting
  6. + *
+ * + * @author Looly + * + */ +public class Profile implements Serializable { + private static final long serialVersionUID = -4189955219454008744L; + + /** 默认环境 */ + public static final String DEFAULT_PROFILE = "default"; + + /** 条件 */ + private String profile; + /** 编码 */ + private Charset charset; + /** 是否使用变量 */ + private boolean useVar; + /** 配置文件缓存 */ + private Map settingMap = new ConcurrentHashMap<>(); + + // -------------------------------------------------------------------------------- Constructor start + /** + * 默认构造,环境使用默认的:default,编码UTF-8,不使用变量 + */ + public Profile() { + this(DEFAULT_PROFILE); + } + + /** + * 构造,编码UTF-8,不使用变量 + * + * @param profile 环境 + */ + public Profile(String profile) { + this(profile, Setting.DEFAULT_CHARSET, false); + } + + /** + * 构造 + * + * @param profile 环境 + * @param charset 编码 + * @param useVar 是否使用变量 + */ + public Profile(String profile, Charset charset, boolean useVar) { + super(); + this.profile = profile; + this.charset = charset; + this.useVar = useVar; + } + // -------------------------------------------------------------------------------- Constructor end + + /** + * 获取当前环境下的配置文件 + * + * @param name 文件名,如果没有扩展名,默认为.setting + * @return 当前环境下配置文件 + */ + public Setting getSetting(String name) { + String nameForProfile = fixNameForProfile(name); + Setting setting = settingMap.get(nameForProfile); + if (null == setting) { + setting = new Setting(nameForProfile, this.charset, this.useVar); + settingMap.put(nameForProfile, setting); + } + return setting; + } + + /** + * 设置环境 + * + * @param profile 环境 + * @return 自身 + */ + public Profile setProfile(String profile) { + this.profile = profile; + return this; + } + + /** + * 设置编码 + * + * @param charset 编码 + * @return 自身 + */ + public Profile setCharset(Charset charset) { + this.charset = charset; + return this; + } + + /** + * 设置是否使用变量 + * + * @param useVar 变量 + * @return 自身 + */ + public Profile setUseVar(boolean useVar) { + this.useVar = useVar; + return this; + } + + /** + * 清空所有环境的配置文件 + * + * @return 自身 + */ + public Profile clear() { + this.settingMap.clear(); + return this; + } + + // -------------------------------------------------------------------------------- Private method start + /** + * 修正文件名 + * + * @param name 文件名 + * @return 修正后的文件名 + */ + private String fixNameForProfile(String name) { + Assert.notBlank(name, "Setting name must be not blank !"); + final String actralProfile = StrUtil.nullToEmpty(this.profile); + if (false == name.contains(StrUtil.DOT)) { + return StrUtil.format("{}/{}.setting", actralProfile, name); + } + return StrUtil.format("{}/{}", actralProfile, name); + } + // -------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-setting/src/main/java/cn/hutool/setting/profile/package-info.java b/hutool-setting/src/main/java/cn/hutool/setting/profile/package-info.java new file mode 100644 index 000000000..79f1ebba5 --- /dev/null +++ b/hutool-setting/src/main/java/cn/hutool/setting/profile/package-info.java @@ -0,0 +1,7 @@ +/** + * 配置文件实现分装,例如Properties封装Props + * + * @author looly + * + */ +package cn.hutool.setting.profile; \ No newline at end of file diff --git a/hutool-setting/src/test/java/cn/hutool/setting/test/PropsTest.java b/hutool-setting/src/test/java/cn/hutool/setting/test/PropsTest.java new file mode 100644 index 000000000..71144461a --- /dev/null +++ b/hutool-setting/src/test/java/cn/hutool/setting/test/PropsTest.java @@ -0,0 +1,44 @@ +package cn.hutool.setting.test; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.log.LogFactory; +import cn.hutool.log.dialect.console.ConsoleLogFactory; +import cn.hutool.setting.dialect.Props; + +/** + * Setting单元测试 + * @author Looly + * + */ +public class PropsTest { + + @Before + public void init(){ + LogFactory.setCurrentLogFactory(ConsoleLogFactory.class); + } + + @Test + public void propTest(){ + Props props = new Props("test.properties"); + String user = props.getProperty("user"); + Assert.assertEquals(user, "root"); + + String driver = props.getStr("driver"); + Assert.assertEquals(driver, "com.mysql.jdbc.Driver"); + } + + @Test + @Ignore + public void propTestForAbsPAth(){ + Props props = new Props("d:/test.properties"); + String user = props.getProperty("user"); + Assert.assertEquals(user, "root"); + + String driver = props.getStr("driver"); + Assert.assertEquals(driver, "com.mysql.jdbc.Driver"); + } +} diff --git a/hutool-setting/src/test/java/cn/hutool/setting/test/SettingTest.java b/hutool-setting/src/test/java/cn/hutool/setting/test/SettingTest.java new file mode 100644 index 000000000..5c1ec1886 --- /dev/null +++ b/hutool-setting/src/test/java/cn/hutool/setting/test/SettingTest.java @@ -0,0 +1,58 @@ +package cn.hutool.setting.test; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import cn.hutool.core.lang.Console; +import cn.hutool.setting.Setting; + +/** + * Setting单元测试 + * @author Looly + * + */ +public class SettingTest { + + @Test + public void settingTest(){ + Setting setting = new Setting("test.setting", true); + + String driver = setting.getByGroup("driver", "demo"); + Assert.assertEquals("com.mysql.jdbc.Driver", driver); + + //本分组变量替换 + String user = setting.getByGroup("user", "demo"); + Assert.assertEquals("rootcom.mysql.jdbc.Driver", user); + + //跨分组变量替换 + String user2 = setting.getByGroup("user2", "demo"); + Assert.assertEquals("rootcom.mysql.jdbc.Driver", user2); + + //默认值测试 + String value = setting.getStr("keyNotExist", "defaultTest"); + Assert.assertEquals("defaultTest", value); + } + + @Test + @Ignore + public void settingTestForAbsPath(){ + Setting setting = new Setting("d:\\excel-plugin\\other.setting", true); + Console.log(setting.getStr("a")); + } + + @Test + public void settingTestForCustom() { + Setting setting = new Setting(); + + setting.put("group1", "user", "root"); + setting.put("group2", "user", "root2"); + setting.put("group3", "user", "root3"); + setting.set("user", "root4"); + + Assert.assertEquals("root", setting.getByGroup("user", "group1")); + Assert.assertEquals("root2", setting.getByGroup("user", "group2")); + Assert.assertEquals("root3", setting.getByGroup("user", "group3")); + Assert.assertEquals("root4", setting.get("user")); + } +} diff --git a/hutool-setting/src/test/java/cn/hutool/setting/test/SettingUtilTest.java b/hutool-setting/src/test/java/cn/hutool/setting/test/SettingUtilTest.java new file mode 100644 index 000000000..b4ffe62e9 --- /dev/null +++ b/hutool-setting/src/test/java/cn/hutool/setting/test/SettingUtilTest.java @@ -0,0 +1,15 @@ +package cn.hutool.setting.test; + +import org.junit.Assert; +import org.junit.Test; + +import cn.hutool.setting.SettingUtil; + +public class SettingUtilTest { + + @Test + public void getTest() { + String driver = SettingUtil.get("test").get("demo", "driver"); + Assert.assertEquals("com.mysql.jdbc.Driver", driver); + } +} diff --git a/hutool-setting/src/test/resources/example/example.set b/hutool-setting/src/test/resources/example/example.set new file mode 100644 index 000000000..ffceff3a6 --- /dev/null +++ b/hutool-setting/src/test/resources/example/example.set @@ -0,0 +1,24 @@ +# ------------------------------------------------------------- +# ----- GroupedSet File with UTF8----- +# @see com.xiaoleilu.hutool.lang.GroupedSet +# ------------------------------------------------------------- + +无分组值1 +无分组值2 + +[分组1] +值1 +值2 +值3 + +[特殊分组] +1 +2 +3 +\#转义注释符 + +[] +空分组值1 +空分组会合并到无分组里 + +[没有值的分组] \ No newline at end of file diff --git a/hutool-setting/src/test/resources/example/example.setting b/hutool-setting/src/test/resources/example/example.setting new file mode 100644 index 000000000..c755c60d9 --- /dev/null +++ b/hutool-setting/src/test/resources/example/example.setting @@ -0,0 +1,20 @@ +# ------------------------------------------------------------- +# ----- Setting File with UTF8----- +# ----- 数据库配置文件 ----- +# ------------------------------------------------------------- + +# 键值都支持中文,默认UTF-8编码,可以在new Setting的时候设置编码 +key = value + +#中括表示一个分组,其下面的所有属性归属于这个分组,在此分组名为demo,也可以没有分组 +#分组后的键值对在Setting对象中表现形式是:demo.key,也可以使用相应的方法取值 +[demo] +# 类似于Properties的键值对 +key = value +# 支持变量替换(在new Setting的时候需要设置isUseVariable为true) +key2 = value${key} + +#中括号开始表示新的分组开始,分组相互独立,键值与其他分组互不影响 +[demo2] +key = value2 +key2 = value \ No newline at end of file diff --git a/hutool-setting/src/test/resources/example/group-set-example.set b/hutool-setting/src/test/resources/example/group-set-example.set new file mode 100644 index 000000000..d41b0802a --- /dev/null +++ b/hutool-setting/src/test/resources/example/group-set-example.set @@ -0,0 +1,13 @@ +#-------------------------------------- +# GroupSet配置文件样例 +#每一个分组下有一组set集合 +# author xiaoleilu +#-------------------------------------- + +[group1] +value1 +value2 + +[group2] +value1 +value2 \ No newline at end of file diff --git a/hutool-setting/src/test/resources/test.properties b/hutool-setting/src/test/resources/test.properties new file mode 100644 index 000000000..2ac552eef --- /dev/null +++ b/hutool-setting/src/test/resources/test.properties @@ -0,0 +1,13 @@ +# ------------------------------------------------------------- +# ----- Setting File with UTF8----- +# ----- \u6570\u636e\u5e93\u914d\u7f6e\u6587\u4ef6 ----- +# ------------------------------------------------------------- + +#\u6570\u636e\u5e93\u9a71\u52a8\u540d\uff0c\u5982\u679c\u4e0d\u6307\u5b9a\uff0c\u5219\u4f1a\u6839\u636eurl\u81ea\u52a8\u5224\u5b9a +driver = com.mysql.jdbc.Driver +#JDBC url\uff0c\u5fc5\u987b +url = jdbc:mysql://fedora.vmware:3306/extractor +#\u7528\u6237\u540d\uff0c\u5fc5\u987b +user = root +#\u5bc6\u7801\uff0c\u5fc5\u987b\uff0c\u5982\u679c\u5bc6\u7801\u4e3a\u7a7a\uff0c\u8bf7\u586b\u5199 pass = +pass = 123456 \ No newline at end of file diff --git a/hutool-setting/src/test/resources/test.setting b/hutool-setting/src/test/resources/test.setting new file mode 100644 index 000000000..39ef65108 --- /dev/null +++ b/hutool-setting/src/test/resources/test.setting @@ -0,0 +1,16 @@ +# ------------------------------------------------------------- +# ----- Setting File with UTF8----- +# ----- 数据库配置文件 ----- +# ------------------------------------------------------------- + +#中括表示一个分组,其下面的所有属性归属于这个分组,在此分组名为demo,也可以没有分组 +[demo] +#数据库驱动名,如果不指定,则会根据url自动判定 +driver = com.mysql.jdbc.Driver +#JDBC url,必须 +url = jdbc:mysql://fedora.vmware:3306/extractor +#用户名,必须 +user = root${driver} +user2 = root${demo.driver} +#密码,必须,如果密码为空,请填写 pass = +pass = 123456 \ No newline at end of file diff --git a/hutool-socket/pom.xml b/hutool-socket/pom.xml new file mode 100644 index 000000000..55815becb --- /dev/null +++ b/hutool-socket/pom.xml @@ -0,0 +1,31 @@ + + + 4.0.0 + + jar + + + cn.hutool + hutool-parent + 4.6.2-SNAPSHOT + + + hutool-socket + ${project.artifactId} + Hutool套接字,包括BIO、NIO、AIO封装 + + + + cn.hutool + hutool-core + ${project.parent.version} + + + cn.hutool + hutool-log + ${project.parent.version} + + + diff --git a/hutool-socket/src/main/java/cn/hutool/socket/SocketConfig.java b/hutool-socket/src/main/java/cn/hutool/socket/SocketConfig.java new file mode 100644 index 000000000..7a43611ee --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/SocketConfig.java @@ -0,0 +1,117 @@ +package cn.hutool.socket; + +import java.io.Serializable; + +import cn.hutool.core.io.IoUtil; + +/** + * Socket通讯配置 + * + * @author looly + * + */ +public class SocketConfig implements Serializable{ + private static final long serialVersionUID = 1L; + + /** CPU核心数 */ + private static int CPU_COUNT = Runtime.getRuntime().availableProcessors(); + + /** 共享线程池大小,此线程池用于接收和处理用户连接 */ + private int threadPoolSize = CPU_COUNT; + + /** 读取超时时长,小于等于0表示默认 */ + private long readTimeout; + /** 写出超时时长,小于等于0表示默认 */ + private long writeTimeout; + + /** 读取缓存大小 */ + private int readBufferSize = IoUtil.DEFAULT_BUFFER_SIZE; + /** 写出缓存大小 */ + private int writeBufferSize = IoUtil.DEFAULT_BUFFER_SIZE; + + /** + * 获取共享线程池大小,此线程池用于接收和处理用户连接 + * + * @return 共享线程池大小,此线程池用于接收和处理用户连接 + */ + public int getThreadPoolSize() { + return threadPoolSize; + } + + /** + * 设置共享线程池大小,此线程池用于接收和处理用户连接 + * + * @param threadPoolSize 共享线程池大小,此线程池用于接收和处理用户连接 + */ + public void setThreadPoolSize(int threadPoolSize) { + this.threadPoolSize = threadPoolSize; + } + + /** + * 获取读取超时时长,小于等于0表示默认 + * + * @return 读取超时时长,小于等于0表示默认 + */ + public long getReadTimeout() { + return readTimeout; + } + + /** + * 设置读取超时时长,小于等于0表示默认 + * + * @param readTimeout 读取超时时长,小于等于0表示默认 + */ + public void setReadTimeout(long readTimeout) { + this.readTimeout = readTimeout; + } + + /** + * 获取写出超时时长,小于等于0表示默认 + * + * @return 写出超时时长,小于等于0表示默认 + */ + public long getWriteTimeout() { + return writeTimeout; + } + + /** + * 设置写出超时时长,小于等于0表示默认 + * + * @param writeTimeout 写出超时时长,小于等于0表示默认 + */ + public void setWriteTimeout(long writeTimeout) { + this.writeTimeout = writeTimeout; + } + + /** + * 获取读取缓存大小 + * @return 读取缓存大小 + */ + public int getReadBufferSize() { + return readBufferSize; + } + + /** + * 设置读取缓存大小 + * @param readBufferSize 读取缓存大小 + */ + public void setReadBufferSize(int readBufferSize) { + this.readBufferSize = readBufferSize; + } + + /** + * 获取写出缓存大小 + * @return 写出缓存大小 + */ + public int getWriteBufferSize() { + return writeBufferSize; + } + + /** + * 设置写出缓存大小 + * @param writeBufferSize 写出缓存大小 + */ + public void setWriteBufferSize(int writeBufferSize) { + this.writeBufferSize = writeBufferSize; + } +} diff --git a/hutool-socket/src/main/java/cn/hutool/socket/SocketRuntimeException.java b/hutool-socket/src/main/java/cn/hutool/socket/SocketRuntimeException.java new file mode 100644 index 000000000..0c0baf63b --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/SocketRuntimeException.java @@ -0,0 +1,33 @@ +package cn.hutool.socket; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.StrUtil; + +/** + * Socket异常 + * + * @author xiaoleilu + */ +public class SocketRuntimeException extends RuntimeException { + private static final long serialVersionUID = 8247610319171014183L; + + public SocketRuntimeException(Throwable e) { + super(ExceptionUtil.getMessage(e), e); + } + + public SocketRuntimeException(String message) { + super(message); + } + + public SocketRuntimeException(String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params)); + } + + public SocketRuntimeException(String message, Throwable throwable) { + super(message, throwable); + } + + public SocketRuntimeException(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } +} diff --git a/hutool-socket/src/main/java/cn/hutool/socket/SocketUtil.java b/hutool-socket/src/main/java/cn/hutool/socket/SocketUtil.java new file mode 100644 index 000000000..0cd856ebb --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/SocketUtil.java @@ -0,0 +1,46 @@ +package cn.hutool.socket; + +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.channels.AsynchronousSocketChannel; +import java.nio.channels.ClosedChannelException; + +import cn.hutool.core.io.IORuntimeException; + +/** + * Socket相关工具类 + * + * @author looly + * @since 4.5.0 + */ +public class SocketUtil { + + /** + * 获取远程端的地址信息,包括host和端口
+ * null表示channel为null或者远程主机未连接 + * + * @param channel {@link AsynchronousSocketChannel} + * @return 远程端的地址信息,包括host和端口,null表示channel为null或者远程主机未连接 + */ + public static SocketAddress getRemoteAddress(AsynchronousSocketChannel channel) { + try { + return (null == channel) ? null : channel.getRemoteAddress(); + } catch (ClosedChannelException e) { + // Channel未打开或已关闭,返回null表示未连接 + return null; + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 远程主机是否处于连接状态
+ * 通过判断远程地址获取成功与否判断 + * + * @param channel {@link AsynchronousSocketChannel} + * @return 远程主机是否处于连接状态 + */ + public static boolean isConnected(AsynchronousSocketChannel channel) { + return null != getRemoteAddress(channel); + } +} diff --git a/hutool-socket/src/main/java/cn/hutool/socket/aio/AcceptHandler.java b/hutool-socket/src/main/java/cn/hutool/socket/aio/AcceptHandler.java new file mode 100644 index 000000000..c2f53b761 --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/aio/AcceptHandler.java @@ -0,0 +1,37 @@ +package cn.hutool.socket.aio; + +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousSocketChannel; +import java.nio.channels.CompletionHandler; + +import cn.hutool.log.StaticLog; + +/** + * 接入完成回调,单例使用 + * + * @author looly + * + */ +public class AcceptHandler implements CompletionHandler { + + @Override + public void completed(AsynchronousSocketChannel socketChannel, AioServer aioServer) { + // 继续等待接入(异步) + aioServer.accept(); + + final IoAction ioAction = aioServer.ioAction; + // 创建Session会话 + final AioSession session = new AioSession(socketChannel, ioAction, aioServer.config); + // 处理请求接入(同步) + ioAction.accept(session); + + // 处理读(异步) + session.read(); + } + + @Override + public void failed(Throwable exc, AioServer aioServer) { + StaticLog.error(exc); + } + +} diff --git a/hutool-socket/src/main/java/cn/hutool/socket/aio/AioClient.java b/hutool-socket/src/main/java/cn/hutool/socket/aio/AioClient.java new file mode 100644 index 000000000..7dd8ca268 --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/aio/AioClient.java @@ -0,0 +1,138 @@ +package cn.hutool.socket.aio; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketOption; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousChannelGroup; +import java.nio.channels.AsynchronousSocketChannel; +import java.util.concurrent.ExecutionException; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.thread.ThreadFactoryBuilder; +import cn.hutool.socket.SocketConfig; +import cn.hutool.socket.SocketRuntimeException; + +/** + * Aio Socket客户端 + * + * @author looly + * @since 4.5.0 + */ +public class AioClient { + + private AioSession session; + + /** + * 构造 + * + * @param address 地址 + * @param ioAction IO处理类 + */ + public AioClient(InetSocketAddress address, IoAction ioAction) { + this(address, ioAction, new SocketConfig()); + } + + /** + * 构造 + * + * @param address 地址 + * @param ioAction IO处理类 + * @param config 配置项 + */ + public AioClient(InetSocketAddress address, IoAction ioAction, SocketConfig config) { + this(createChannel(address, config.getThreadPoolSize()), ioAction, config); + } + + /** + * 构造 + * + * @param channel {@link AsynchronousSocketChannel} + * @param ioAction IO处理类 + * @param config 配置项 + */ + public AioClient(AsynchronousSocketChannel channel, IoAction ioAction, SocketConfig config) { + this.session = new AioSession(channel, ioAction, config); + ioAction.accept(this.session); + } + + /** + * 设置 Socket 的 Option 选项
+ * 选项见:{@link java.net.StandardSocketOptions} + * + * @param 选项泛型 + * @param name {@link SocketOption} 枚举 + * @param value SocketOption参数 + * @throws IOException IO异常 + */ + public AioClient setOption(SocketOption name, T value) throws IOException { + this.session.getChannel().setOption(name, value); + return this; + } + + /** + * 获取IO处理器 + * + * @return {@link IoAction} + */ + public IoAction getIoAction() { + return this.session.getIoAction(); + } + + /** + * 从服务端读取数据 + * + * @return this + */ + public AioClient read() { + this.session.read(); + return this; + } + + /** + * 写数据到服务端 + * + * @return this + */ + public AioClient write(ByteBuffer data) { + this.session.write(data); + return this; + } + + /** + * 关闭客户端 + */ + public void close() { + this.session.close(); + } + + // ------------------------------------------------------------------------------------- Private method start + /** + * 初始化 + * + * @param address 地址和端口 + * @param poolSize 线程池大小 + * @return this + */ + private static AsynchronousSocketChannel createChannel(InetSocketAddress address, int poolSize) { + + AsynchronousSocketChannel channel; + try { + AsynchronousChannelGroup group = AsynchronousChannelGroup.withFixedThreadPool(// + poolSize, // 默认线程池大小 + ThreadFactoryBuilder.create().setNamePrefix("Huool-socket-").build()// + ); + channel = AsynchronousSocketChannel.open(group); + } catch (IOException e) { + throw new IORuntimeException(e); + } + + try { + channel.connect(address).get(); + } catch (InterruptedException | ExecutionException e) { + throw new SocketRuntimeException(e); + } + return channel; + } + // ------------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-socket/src/main/java/cn/hutool/socket/aio/AioServer.java b/hutool-socket/src/main/java/cn/hutool/socket/aio/AioServer.java new file mode 100644 index 000000000..fc8a57153 --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/aio/AioServer.java @@ -0,0 +1,186 @@ +package cn.hutool.socket.aio; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketOption; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousChannelGroup; +import java.nio.channels.AsynchronousServerSocketChannel; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.thread.ThreadFactoryBuilder; +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import cn.hutool.socket.SocketConfig; + +/** + * 基于AIO的Socket服务端实现 + * + * @author looly + * + */ +public class AioServer { + private static final Log log = LogFactory.get(); + private static AcceptHandler ACCEPT_HANDLER = new AcceptHandler(); + + private AsynchronousChannelGroup group; + private AsynchronousServerSocketChannel channel; + protected IoAction ioAction; + protected SocketConfig config; + + + /** + * 构造 + * + * @param port 端口 + */ + public AioServer(int port) { + this(new InetSocketAddress(port), new SocketConfig()); + } + + /** + * 构造 + * + * @param address 地址 + * @param config {@link SocketConfig} 配置项 + */ + public AioServer(InetSocketAddress address, SocketConfig config) { + this.config = config; + init(address); + } + + /** + * 初始化 + * + * @param address 地址和端口 + * @return this + */ + public AioServer init(InetSocketAddress address) { + try { + this.group = AsynchronousChannelGroup.withFixedThreadPool(// + config.getThreadPoolSize(), // 默认线程池大小 + ThreadFactoryBuilder.create().setNamePrefix("Hutool-socket-").build()// + ); + this.channel = AsynchronousServerSocketChannel.open(group).bind(address); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return this; + } + + /** + * 开始监听 + * + * @param sync 是否阻塞 + */ + public void start(boolean sync) { + try { + doStart(sync); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 设置 Socket 的 Option 选项
+ * 选项见:{@link java.net.StandardSocketOptions} + * + * @param 选项泛型 + * @param name {@link SocketOption} 枚举 + * @param value SocketOption参数 + * @throws IOException IO异常 + */ + public AioServer setOption(SocketOption name, T value) throws IOException { + this.channel.setOption(name, value); + return this; + } + + /** + * 获取IO处理器 + * + * @return {@link IoAction} + */ + public IoAction getIoAction() { + return this.ioAction; + } + + /** + * 设置IO处理器,单例存在 + * + * @param ioAction {@link IoAction} + * @return this; + */ + public AioServer setIoAction(IoAction ioAction) { + this.ioAction = ioAction; + return this; + } + + /** + * 获取{@link AsynchronousServerSocketChannel} + * + * @return {@link AsynchronousServerSocketChannel} + */ + public AsynchronousServerSocketChannel getChannel() { + return this.channel; + } + + /** + * 处理接入的客户端 + * + * @return this + */ + public AioServer accept() { + this.channel.accept(this, ACCEPT_HANDLER); + return this; + } + + /** + * 服务是否开启状态 + * + * @return 服务是否开启状态 + */ + public boolean isOpen() { + return (null == this.channel) ? false : this.channel.isOpen(); + } + + /** + * 关闭服务 + */ + public void close() { + IoUtil.close(this.channel); + + if (null != this.group && false == this.group.isShutdown()) { + try { + this.group.shutdownNow(); + } catch (IOException e) { + // ignore + } + } + + // 结束阻塞 + synchronized (this) { + this.notify(); + } + } + + // ------------------------------------------------------------------------------------- Private method start + /** + * 开始监听 + * + * @param sync 是否阻塞 + * @throws IOException IO异常 + */ + private void doStart(boolean sync) throws IOException { + log.debug("Aio Server started, waiting for accept."); + + // 接收客户端连接 + accept(); + + if (sync) { + ThreadUtil.sync(this); + } + } + // ------------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-socket/src/main/java/cn/hutool/socket/aio/AioSession.java b/hutool-socket/src/main/java/cn/hutool/socket/aio/AioSession.java new file mode 100644 index 000000000..b16a7b3f2 --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/aio/AioSession.java @@ -0,0 +1,206 @@ +package cn.hutool.socket.aio; + +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousSocketChannel; +import java.nio.channels.CompletionHandler; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.socket.SocketConfig; +import cn.hutool.socket.SocketUtil; + +/** + * AIO会话
+ * 每个客户端对应一个会话对象 + * + * @author looly + * + */ +public class AioSession { + + private static final ReadHandler READ_HANDLER = new ReadHandler(); + + private AsynchronousSocketChannel channel; + private IoAction ioAction; + private ByteBuffer readBuffer; + private ByteBuffer writeBuffer; + /** 读取超时时长,小于等于0表示默认 */ + private long readTimeout; + /** 写出超时时长,小于等于0表示默认 */ + private long writeTimeout; + + /** + * 构造 + * + * @param channel {@link AsynchronousSocketChannel} + * @param ioAction IO消息处理类 + * @param config 配置项 + */ + public AioSession(AsynchronousSocketChannel channel, IoAction ioAction, SocketConfig config) { + this.channel = channel; + this.readBuffer = ByteBuffer.allocate(config.getReadBufferSize()); + this.writeBuffer = ByteBuffer.allocate(config.getWriteBufferSize()); + this.ioAction = ioAction; + } + + /** + * 获取{@link AsynchronousSocketChannel} + * + * @return {@link AsynchronousSocketChannel} + */ + public AsynchronousSocketChannel getChannel() { + return this.channel; + } + + /** + * 获取读取Buffer + * + * @return 读取Buffer + */ + public ByteBuffer getReadBuffer() { + return this.readBuffer; + } + + /** + * 获取写Buffer + * + * @return 写Buffer + */ + public ByteBuffer getWriteBuffer() { + return this.writeBuffer; + } + + /** + * 获取消息处理器 + * + * @return {@link IoAction} + */ + public IoAction getIoAction() { + return this.ioAction; + } + + /** + * 获取远程主机(客户端)地址和端口 + * + * @return 远程主机(客户端)地址和端口 + */ + public SocketAddress getRemoteAddress() { + return SocketUtil.getRemoteAddress(this.channel); + } + + /** + * 读取数据到Buffer + * + * @return this + */ + public AioSession read() { + return read(READ_HANDLER); + } + + /** + * 读取数据到Buffer + * + * @param handler {@link CompletionHandler} + * @return this + */ + public AioSession read(CompletionHandler handler) { + if (isOpen()) { + this.readBuffer.clear(); + this.channel.read(this.readBuffer, Math.max(this.readTimeout, 0L), TimeUnit.MILLISECONDS, this, handler); + } + return this; + } + + /** + * 写数据到目标端,并关闭输出 + * + * @return this + */ + public AioSession writeAndClose(ByteBuffer data) { + write(data); + return closeOut(); + } + + /** + * 写数据到目标端 + * + * @return {@link Future} + */ + public Future write(ByteBuffer data) { + return this.channel.write(data); + } + + /** + * 写数据到目标端 + * + * @param handler {@link CompletionHandler} + * @return this + */ + public AioSession write(ByteBuffer data, CompletionHandler handler) { + this.channel.write(data, Math.max(this.writeTimeout, 0L), TimeUnit.MILLISECONDS, this, handler); + return this; + } + + /** + * 会话是否打开状态
+ * 当Socket保持连接时会话始终打开 + * + * @return 会话是否打开状态 + */ + public boolean isOpen() { + return (null == this.channel) ? false : this.channel.isOpen(); + } + + /** + * 关闭输出 + * + * @return this + */ + public AioSession closeIn() { + if (null != this.channel) { + try { + this.channel.shutdownInput(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + return this; + } + + /** + * 关闭输出 + * + * @return this + */ + public AioSession closeOut() { + if (null != this.channel) { + try { + this.channel.shutdownOutput(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + return this; + } + + /** + * 关闭会话 + */ + public void close() { + IoUtil.close(this.channel); + this.readBuffer = null; + this.writeBuffer = null; + } + + /** + * 执行读,用于读取事件结束的回调 + */ + protected void callbackRead() { + readBuffer.flip();// 读模式 + ioAction.doAction(this, readBuffer); + } +} diff --git a/hutool-socket/src/main/java/cn/hutool/socket/aio/IoAction.java b/hutool-socket/src/main/java/cn/hutool/socket/aio/IoAction.java new file mode 100644 index 000000000..b871d271c --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/aio/IoAction.java @@ -0,0 +1,35 @@ +package cn.hutool.socket.aio; + +/** + * Socket流处理接口
+ * 实现此接口用于处理接收到的消息,发送指定消息 + * + * @author looly + * + * @param 经过解码器解码后的数据类型 + */ +public interface IoAction { + + /** + * 接收客户端连接(会话建立)事件处理 + * + * @param session 会话 + */ + void accept(AioSession session); + + /** + * 执行数据处理(消息读取) + * + * @param session Socket Session会话 + * @param data 解码后的数据 + */ + void doAction(AioSession session, T data); + + /** + * 数据读取失败的回调事件处理(消息读取失败) + * + * @param exc 异常 + * @param session Session + */ + void failed(Throwable exc, AioSession session); +} diff --git a/hutool-socket/src/main/java/cn/hutool/socket/aio/ReadHandler.java b/hutool-socket/src/main/java/cn/hutool/socket/aio/ReadHandler.java new file mode 100644 index 000000000..73d7e4338 --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/aio/ReadHandler.java @@ -0,0 +1,25 @@ +package cn.hutool.socket.aio; + +import java.nio.channels.CompletionHandler; + +import cn.hutool.socket.SocketRuntimeException; + +/** + * 数据读取完成回调,调用Session中相应方法处理消息,单例使用 + * + * @author looly + * + */ +public class ReadHandler implements CompletionHandler { + + @Override + public void completed(Integer result, AioSession session) { + session.callbackRead(); + } + + @Override + public void failed(Throwable exc, AioSession session) { + throw new SocketRuntimeException(exc); + } + +} diff --git a/hutool-socket/src/main/java/cn/hutool/socket/aio/SimpleIoAction.java b/hutool-socket/src/main/java/cn/hutool/socket/aio/SimpleIoAction.java new file mode 100644 index 000000000..8e891e42b --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/aio/SimpleIoAction.java @@ -0,0 +1,24 @@ +package cn.hutool.socket.aio; + +import java.nio.ByteBuffer; + +import cn.hutool.log.StaticLog; + +/** + * 简易IO信息处理类
+ * 简单实现了accept和failed事件 + * + * @author looly + * + */ +public abstract class SimpleIoAction implements IoAction { + + @Override + public void accept(AioSession session) { + } + + @Override + public void failed(Throwable exc, AioSession session) { + StaticLog.error(exc); + } +} diff --git a/hutool-socket/src/main/java/cn/hutool/socket/aio/package-info.java b/hutool-socket/src/main/java/cn/hutool/socket/aio/package-info.java new file mode 100644 index 000000000..57339acf3 --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/aio/package-info.java @@ -0,0 +1,7 @@ +/** + * AIO相关封装 + * + * @author looly + * + */ +package cn.hutool.socket.aio; \ No newline at end of file diff --git a/hutool-socket/src/main/java/cn/hutool/socket/nio/NioClient.java b/hutool-socket/src/main/java/cn/hutool/socket/nio/NioClient.java new file mode 100644 index 000000000..ebe9470a0 --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/nio/NioClient.java @@ -0,0 +1,83 @@ +package cn.hutool.socket.nio; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; + +import cn.hutool.core.io.IORuntimeException; + +/** + * NIO客户端 + * + * @author looly + * @since 4.4.5 + */ +public class NioClient { + + private SocketChannel channel; + + /** + * 构造 + * + * @param host 服务器地址 + * @param port 端口 + */ + public NioClient(String host, int port) { + init(new InetSocketAddress(host, port)); + } + + /** + * 构造 + * + * @param address 服务器地址 + */ + public NioClient(InetSocketAddress address) { + init(address); + } + + /** + * 初始化 + * + * @param address 地址和端口 + * @return this + */ + public NioClient init(InetSocketAddress address) { + try { + this.channel = SocketChannel.open(address); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return this; + } + + /** + * 处理读事件
+ * 当收到读取准备就绪的信号后,回调此方法,用户可读取从客户端传世来的消息 + * + * @param buffer 服务端数据存储缓存 + */ + public NioClient read(ByteBuffer buffer) { + try { + this.channel.read(buffer); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return this; + } + + /** + * 实现写逻辑
+ * 当收到写出准备就绪的信号后,回调此方法,用户可向客户端发送消息 + * + * @param datas 发送的数据 + */ + public NioClient write(ByteBuffer... datas) { + try { + this.channel.write(datas); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return this; + } +} diff --git a/hutool-socket/src/main/java/cn/hutool/socket/nio/NioServer.java b/hutool-socket/src/main/java/cn/hutool/socket/nio/NioServer.java new file mode 100644 index 000000000..c5c002e3c --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/nio/NioServer.java @@ -0,0 +1,174 @@ +package cn.hutool.socket.nio; + +import java.io.Closeable; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.Iterator; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; + +/** + * 基于NIO的Socket服务端实现 + * + * @author looly + * + */ +public abstract class NioServer implements Closeable { + + private Selector selector; + private ServerSocketChannel serverSocketChannel; + + /** + * 构造 + * + * @param port 端口 + */ + public NioServer(int port) { + init(new InetSocketAddress(port)); + } + + /** + * 初始化 + * + * @param address 地址和端口 + * @return this + */ + public NioServer init(InetSocketAddress address) { + try { + // 打开服务器套接字通道 + this.serverSocketChannel = ServerSocketChannel.open(); + // 设置为非阻塞状态 + serverSocketChannel.configureBlocking(false); + // 获取通道相关联的套接字 + final ServerSocket serverSocket = serverSocketChannel.socket(); + // 绑定端口号 + serverSocket.bind(address); + + // 打开一个选择器 + selector = Selector.open(); + // 服务器套接字注册到Selector中 并指定Selector监控连接事件 + serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); + } catch (IOException e) { + throw new IORuntimeException(e); + } + + return this; + } + + /** + * 开始监听 + */ + public void listen() { + try { + doListen(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 开始监听 + * + * @throws IOException IO异常 + */ + private void doListen() throws IOException { + while (0 != this.selector.select()) { + // 返回已选择键的集合 + final Iterator keyIter = selector.selectedKeys().iterator(); + while (keyIter.hasNext()) { + handle(keyIter.next()); + keyIter.remove(); + } + } + } + + /** + * 处理SelectionKey + * + * @param key SelectionKey + */ + private void handle(SelectionKey key) { + // 有客户端接入此服务端 + if (key.isAcceptable()) { + // 获取通道 转化为要处理的类型 + final ServerSocketChannel server = (ServerSocketChannel) key.channel(); + SocketChannel socketChannel; + try { + // 获取连接到此服务器的客户端通道 + socketChannel = server.accept(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + + // SocketChannel通道的可读事件注册到Selector中 + registerChannel(selector, socketChannel, Operation.READ); + } + + // 读事件就绪 + if (key.isReadable()) { + final SocketChannel socketChannel = (SocketChannel) key.channel(); + read(socketChannel); + + // SocketChannel通道的可写事件注册到Selector中 + registerChannel(selector, socketChannel, Operation.WRITE); + } + + // 写事件就绪 + if (key.isWritable()) { + final SocketChannel socketChannel = (SocketChannel) key.channel(); + write(socketChannel); + // SocketChannel通道的可读事件注册到Selector中 + registerChannel(selector, socketChannel, Operation.READ); + } + } + + @Override + public void close() throws IOException { + IoUtil.close(this.selector); + IoUtil.close(this.serverSocketChannel); + } + + /** + * 处理读事件
+ * 当收到读取准备就绪的信号后,回调此方法,用户可读取从客户端传世来的消息 + * + * @param socketChannel SocketChannel + */ + protected abstract void read(SocketChannel socketChannel); + + /** + * 实现写逻辑
+ * 当收到写出准备就绪的信号后,回调此方法,用户可向客户端发送消息 + * + * @param socketChannel SocketChannel + */ + protected abstract void write(SocketChannel socketChannel); + + /** + * 注册通道到指定Selector上 + * + * @param selector Selector + * @param channel 通道 + * @param ops 注册的通道监听类型 + */ + private void registerChannel(Selector selector, SelectableChannel channel, Operation ops) { + if (channel == null) { + return; + } + + try { + channel.configureBlocking(false); + // 注册通道 + channel.register(selector, ops.getValue()); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } +} diff --git a/hutool-socket/src/main/java/cn/hutool/socket/nio/Operation.java b/hutool-socket/src/main/java/cn/hutool/socket/nio/Operation.java new file mode 100644 index 000000000..e231d54be --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/nio/Operation.java @@ -0,0 +1,48 @@ +package cn.hutool.socket.nio; + +import java.nio.channels.SelectionKey; + +/** + * SelectionKey Operation的枚举封装 + * + * @author looly + */ +public enum Operation { + + /** 读操作 */ + READ(SelectionKey.OP_READ), + /** 写操作 */ + WRITE(SelectionKey.OP_WRITE), + /** 连接操作 */ + CONNECT(SelectionKey.OP_CONNECT), + /** 接受连接操作 */ + ACCEPT(SelectionKey.OP_ACCEPT); + + private int value; + + /** + * 构造 + * + * @param value 值 + * @see SelectionKey#OP_READ + * @see SelectionKey#OP_WRITE + * @see SelectionKey#OP_CONNECT + * @see SelectionKey#OP_ACCEPT + */ + private Operation(int value) { + this.value = value; + } + + /** + * 获取值 + * + * @return 值 + * @see SelectionKey#OP_READ + * @see SelectionKey#OP_WRITE + * @see SelectionKey#OP_CONNECT + * @see SelectionKey#OP_ACCEPT + */ + public int getValue() { + return this.value; + } +} diff --git a/hutool-socket/src/main/java/cn/hutool/socket/nio/package-info.java b/hutool-socket/src/main/java/cn/hutool/socket/nio/package-info.java new file mode 100644 index 000000000..fee9128e1 --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/nio/package-info.java @@ -0,0 +1,7 @@ +/** + * NIO相关封装 + * + * @author looly + * + */ +package cn.hutool.socket.nio; \ No newline at end of file diff --git a/hutool-socket/src/main/java/cn/hutool/socket/package-info.java b/hutool-socket/src/main/java/cn/hutool/socket/package-info.java new file mode 100644 index 000000000..c6e6affbb --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/package-info.java @@ -0,0 +1,7 @@ +/** + * Socket套接字相关工具类封装 + * + * @author looly + * + */ +package cn.hutool.socket; \ No newline at end of file diff --git a/hutool-socket/src/main/java/cn/hutool/socket/protocol/MsgDecoder.java b/hutool-socket/src/main/java/cn/hutool/socket/protocol/MsgDecoder.java new file mode 100644 index 000000000..243380d29 --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/protocol/MsgDecoder.java @@ -0,0 +1,24 @@ +package cn.hutool.socket.protocol; + +import java.nio.ByteBuffer; + +import cn.hutool.socket.aio.AioSession; + +/** + * 消息解码器 + * + * @author looly + * + * @param 解码后的目标类型 + */ +public interface MsgDecoder { + /** + * 对于从Socket流中获取到的数据采用当前MsgDecoder的实现类协议进行解析。 + * + * + * @param session 本次需要解码的session + * @param readBuffer 待处理的读buffer + * @return 本次解码成功后封装的业务消息对象, 返回null则表示解码未完成 + */ + T decode(AioSession session, ByteBuffer readBuffer); +} diff --git a/hutool-socket/src/main/java/cn/hutool/socket/protocol/MsgEncoder.java b/hutool-socket/src/main/java/cn/hutool/socket/protocol/MsgEncoder.java new file mode 100644 index 000000000..8e563a4ad --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/protocol/MsgEncoder.java @@ -0,0 +1,23 @@ +package cn.hutool.socket.protocol; + +import java.nio.ByteBuffer; + +import cn.hutool.socket.aio.AioSession; + +/** + * 消息编码器 + * + * @author looly + * + * @param 编码前后的数据类型 + */ +public interface MsgEncoder { + /** + * 编码数据用于写出 + * + * @param session 本次需要解码的session + * @param writeBuffer 待处理的读buffer + * @param data 写出的数据 + */ + void encode(AioSession session, ByteBuffer writeBuffer, T data); +} diff --git a/hutool-socket/src/main/java/cn/hutool/socket/protocol/Protocol.java b/hutool-socket/src/main/java/cn/hutool/socket/protocol/Protocol.java new file mode 100644 index 000000000..7e9fa5398 --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/protocol/Protocol.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2017, org.smartboot. All rights reserved. + * project name: smart-socket + * file name: Protocol.java + * Date: 2017-11-25 + * Author: sandao + */ + +package cn.hutool.socket.protocol; + +/** + * 协议接口
+ * 通过实现此接口完成消息的编码和解码 + * + *

+ * 所有Socket使用相同协议对象,类成员变量和对象成员变量易造成并发读写问题。 + *

+ * + * @author Looly + */ +public interface Protocol extends MsgEncoder, MsgDecoder { + +} diff --git a/hutool-socket/src/main/java/cn/hutool/socket/protocol/package-info.java b/hutool-socket/src/main/java/cn/hutool/socket/protocol/package-info.java new file mode 100644 index 000000000..edb069568 --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/protocol/package-info.java @@ -0,0 +1,7 @@ +/** + * 消息协议接口及实现 + * + * @author looly + * + */ +package cn.hutool.socket.protocol; \ No newline at end of file diff --git a/hutool-socket/src/test/java/cn/hutool/socket/AioClientTest.java b/hutool-socket/src/test/java/cn/hutool/socket/AioClientTest.java new file mode 100644 index 000000000..872c3cee7 --- /dev/null +++ b/hutool-socket/src/test/java/cn/hutool/socket/AioClientTest.java @@ -0,0 +1,31 @@ +package cn.hutool.socket; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; + +import cn.hutool.core.lang.Console; +import cn.hutool.core.util.StrUtil; +import cn.hutool.socket.aio.AioClient; +import cn.hutool.socket.aio.AioSession; +import cn.hutool.socket.aio.SimpleIoAction; + +public class AioClientTest { + public static void main(String[] args) { + AioClient client = new AioClient(new InetSocketAddress("localhost", 8899), new SimpleIoAction() { + + @Override + public void doAction(AioSession session, ByteBuffer data) { + if(data.hasRemaining()) { + Console.log(StrUtil.utf8Str(data)); + session.read(); + } + Console.log("OK"); + } + }); + + client.write(ByteBuffer.wrap("Hello".getBytes())); + client.read(); + + client.close(); + } +} diff --git a/hutool-socket/src/test/java/cn/hutool/socket/AioServerTest.java b/hutool-socket/src/test/java/cn/hutool/socket/AioServerTest.java new file mode 100644 index 000000000..6fecb90f0 --- /dev/null +++ b/hutool-socket/src/test/java/cn/hutool/socket/AioServerTest.java @@ -0,0 +1,45 @@ +package cn.hutool.socket; + +import java.nio.ByteBuffer; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.io.BufferUtil; +import cn.hutool.core.lang.Console; +import cn.hutool.core.util.StrUtil; +import cn.hutool.log.StaticLog; +import cn.hutool.socket.aio.AioServer; +import cn.hutool.socket.aio.AioSession; +import cn.hutool.socket.aio.SimpleIoAction; + +public class AioServerTest { + + public static void main(String[] args) { + + AioServer aioServer = new AioServer(8899); + aioServer.setIoAction(new SimpleIoAction() { + + @Override + public void accept(AioSession session) { + StaticLog.debug("【客户端】:{} 连接。", session.getRemoteAddress()); + session.write(BufferUtil.createUtf8("=== Welcome to Hutool socket server. ===")); + } + + @Override + public void doAction(AioSession session, ByteBuffer data) { + Console.log(data); + + if(false == data.hasRemaining()) { + StringBuilder response = StrUtil.builder()// + .append("HTTP/1.1 200 OK\r\n")// + .append("Date: ").append(DateUtil.formatHttpDate(DateUtil.date())).append("\r\n")// + .append("Content-Type: text/html; charset=UTF-8\r\n")// + .append("\r\n") + .append("Hello Hutool socket");// + session.writeAndClose(BufferUtil.createUtf8(response)); + }else { + session.read(); + } + } + }).start(true); + } +} diff --git a/hutool-system/pom.xml b/hutool-system/pom.xml new file mode 100644 index 000000000..bef397f23 --- /dev/null +++ b/hutool-system/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + jar + + + cn.hutool + hutool-parent + 4.6.2-SNAPSHOT + + + hutool-system + ${project.artifactId} + Hutool 系统调用(Runtime) + + + + cn.hutool + hutool-core + ${project.parent.version} + + + diff --git a/hutool-system/src/main/java/cn/hutool/system/HostInfo.java b/hutool-system/src/main/java/cn/hutool/system/HostInfo.java new file mode 100644 index 000000000..34c08f5c1 --- /dev/null +++ b/hutool-system/src/main/java/cn/hutool/system/HostInfo.java @@ -0,0 +1,75 @@ +package cn.hutool.system; + +import java.io.Serializable; +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * 代表当前主机的信息。 + */ +public class HostInfo implements Serializable{ + private static final long serialVersionUID = 1L; + + private final String HOST_NAME; + private final String HOST_ADDRESS; + + public HostInfo() { + String hostName; + String hostAddress; + + try { + InetAddress localhost = InetAddress.getLocalHost(); + + hostName = localhost.getHostName(); + hostAddress = localhost.getHostAddress(); + } catch (UnknownHostException e) { + hostName = "localhost"; + hostAddress = "127.0.0.1"; + } + + HOST_NAME = hostName; + HOST_ADDRESS = hostAddress; + } + + /** + * 取得当前主机的名称。 + * + *

+ * 例如:"webserver1" + *

+ * + * @return 主机名 + */ + public final String getName() { + return HOST_NAME; + } + + /** + * 取得当前主机的地址。 + * + *

+ * 例如:"192.168.0.1" + *

+ * + * @return 主机地址 + */ + public final String getAddress() { + return HOST_ADDRESS; + } + + /** + * 将当前主机的信息转换成字符串。 + * + * @return 主机信息的字符串表示 + */ + @Override + public final String toString() { + StringBuilder builder = new StringBuilder(); + + SystemUtil.append(builder, "Host Name: ", getName()); + SystemUtil.append(builder, "Host Address: ", getAddress()); + + return builder.toString(); + } + +} diff --git a/hutool-system/src/main/java/cn/hutool/system/JavaInfo.java b/hutool-system/src/main/java/cn/hutool/system/JavaInfo.java new file mode 100644 index 000000000..98c6b3ff4 --- /dev/null +++ b/hutool-system/src/main/java/cn/hutool/system/JavaInfo.java @@ -0,0 +1,393 @@ +package cn.hutool.system; + +import cn.hutool.core.util.ReUtil; + +import java.io.Serializable; + +/** + * 代表Java Implementation的信息。 + */ +public class JavaInfo implements Serializable{ + private static final long serialVersionUID = 1L; + + private final String JAVA_VERSION = SystemUtil.get("java.version", false); + private final float JAVA_VERSION_FLOAT = getJavaVersionAsFloat(); + private final int JAVA_VERSION_INT = getJavaVersionAsInt(); + private final String JAVA_VENDOR = SystemUtil.get("java.vendor", false); + private final String JAVA_VENDOR_URL = SystemUtil.get("java.vendor.url", false); + + // 1.1--1.3能否识别? + private final boolean IS_JAVA_1_1 = getJavaVersionMatches("1.1"); + private final boolean IS_JAVA_1_2 = getJavaVersionMatches("1.2"); + private final boolean IS_JAVA_1_3 = getJavaVersionMatches("1.3"); + private final boolean IS_JAVA_1_4 = getJavaVersionMatches("1.4"); + private final boolean IS_JAVA_1_5 = getJavaVersionMatches("1.5"); + private final boolean IS_JAVA_1_6 = getJavaVersionMatches("1.6"); + private final boolean IS_JAVA_1_7 = getJavaVersionMatches("1.7"); + private final boolean IS_JAVA_1_8 = getJavaVersionMatches("1.8"); + private final boolean IS_JAVA_9 = getJavaVersionMatches("9"); + private final boolean IS_JAVA_10 = getJavaVersionMatches("10"); + private final boolean IS_JAVA_11 = getJavaVersionMatches("11"); + private final boolean IS_JAVA_12 = getJavaVersionMatches("12"); + + /** + * 取得当前Java impl.的版本(取自系统属性:java.version)。 + * + *

+ * 例如Sun JDK 1.4.2:"1.4.2" + * + * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + * @since Java 1.1 + */ + public final String getVersion() { + return JAVA_VERSION; + } + + /** + * 取得当前Java impl.的版本(取自系统属性:java.version)。 + * + *

+ * 例如: + * + *

    + *
  • JDK 1.2:1.2f
  • + *
  • JDK 1.3.1:1.31f
  • + *
+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回0。 + */ + public final float getVersionFloat() { + return JAVA_VERSION_FLOAT; + } + + /** + * 取得当前Java impl.的版本(取自系统属性:java.version),java10及其之后的版本返回值为4位。 + * + *

+ * 例如: + * + *

    + *
  • JDK 1.2:120
  • + *
  • JDK 1.3.1:131
  • + *
  • JDK 11.0.2:1102
  • + *
+ * + * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回0。 + * + * @since Java 1.1 + */ + public final int getVersionInt() { + return JAVA_VERSION_INT; + } + + /** + * 取得当前Java impl.的版本的float值。 + * + * @return Java版本的float值或0 + */ + private final float getJavaVersionAsFloat() { + if (JAVA_VERSION == null) { + return 0f; + } + + String str = JAVA_VERSION; + + str = ReUtil.get("^[0-9]{1,2}(\\.[0-9]{1,2})?", str,0); + + return Float.parseFloat(str); + } + + /** + * 取得当前Java impl.的版本的int值。 + * + * @return Java版本的int值或0 + */ + private final int getJavaVersionAsInt() { + if (JAVA_VERSION == null) { + return 0; + } + + String java_version = JAVA_VERSION; + + java_version = ReUtil.get("^[0-9]{1,2}(\\.[0-9]{1,2}){0,2}", java_version,0); + + String[] split = java_version.split("\\."); + + String result = ""; + + for (int i = 0; i < split.length; i++) { + result = result + split[i]; + } + + //保证java10及其之后的版本返回的值为4位 + if (split[0].length()>1 && result.length()!=4){ + result = result + "0000"; + result = result.substring(0,4); + } + + return Integer.parseInt(result); + } + + /** + * 取得当前Java impl.的厂商(取自系统属性:java.vendor)。 + * + *

+ * 例如Sun JDK 1.4.2:"Sun Microsystems Inc." + * + * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + * @since Java 1.1 + */ + public final String getVendor() { + return JAVA_VENDOR; + } + + /** + * 取得当前Java impl.的厂商网站的URL(取自系统属性:java.vendor.url)。 + * + *

+ * 例如Sun JDK 1.4.2:"http://java.sun.com/" + * + * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + * @since Java 1.1 + */ + public final String getVendorURL() { + return JAVA_VENDOR_URL; + } + + /** + * 判断当前Java的版本。 + * + *

+ * 如果不能取得系统属性java.version(因为Java安全限制),则总是返回 false + * + * + * @return 如果当前Java版本为1.1,则返回true + */ + public final boolean isJava1_1() { + return IS_JAVA_1_1; + } + + /** + * 判断当前Java的版本。 + * + *

+ * 如果不能取得系统属性java.version(因为Java安全限制),则总是返回 false + * + * + * @return 如果当前Java版本为1.2,则返回true + */ + public final boolean isJava1_2() { + return IS_JAVA_1_2; + } + + /** + * 判断当前Java的版本。 + * + *

+ * 如果不能取得系统属性java.version(因为Java安全限制),则总是返回 false + * + * + * @return 如果当前Java版本为1.3,则返回true + */ + public final boolean isJava1_3() { + return IS_JAVA_1_3; + } + + /** + * 判断当前Java的版本。 + * + *

+ * 如果不能取得系统属性java.version(因为Java安全限制),则总是返回 false + * + * + * @return 如果当前Java版本为1.4,则返回true + */ + public final boolean isJava1_4() { + return IS_JAVA_1_4; + } + + /** + * 判断当前Java的版本。 + * + *

+ * 如果不能取得系统属性java.version(因为Java安全限制),则总是返回 false + * + * + * @return 如果当前Java版本为1.5,则返回true + */ + public final boolean isJava1_5() { + return IS_JAVA_1_5; + } + + /** + * 判断当前Java的版本。 + * + *

+ * 如果不能取得系统属性java.version(因为Java安全限制),则总是返回 false + * + * + * @return 如果当前Java版本为1.6,则返回true + */ + public final boolean isJava1_6() { + return IS_JAVA_1_6; + } + + /** + * 判断当前Java的版本。 + * + *

+ * 如果不能取得系统属性java.version(因为Java安全限制),则总是返回 false + * + * + * @return 如果当前Java版本为1.7,则返回true + */ + public final boolean isJava1_7() { + return IS_JAVA_1_7; + } + + /** + * 判断当前Java的版本。 + * + *

+ * 如果不能取得系统属性java.version(因为Java安全限制),则总是返回 false + * + * + * @return 如果当前Java版本为1.8,则返回true + */ + public final boolean isJava1_8() { + return IS_JAVA_1_8; + } + + /** + * 判断当前Java的版本。 + * + *

+ * 如果不能取得系统属性java.version(因为Java安全限制),则总是返回 false + * + * + * @return 如果当前Java版本为9,则返回true + */ + public final boolean isJava9() { + return IS_JAVA_9; + } + + /** + * 判断当前Java的版本。 + * + *

+ * 如果不能取得系统属性java.version(因为Java安全限制),则总是返回 false + * + * + * @return 如果当前Java版本为10,则返回true + */ + public final boolean isJava10() { + return IS_JAVA_10; + } + + /** + * 判断当前Java的版本。 + * + *

+ * 如果不能取得系统属性java.version(因为Java安全限制),则总是返回 false + * + * + * @return 如果当前Java版本为11,则返回true + */ + public final boolean isJava11() { + return IS_JAVA_11; + } + + /** + * 判断当前Java的版本。 + * + *

+ * 如果不能取得系统属性java.version(因为Java安全限制),则总是返回 false + * + * + * @return 如果当前Java版本为12,则返回true + */ + public final boolean isJava12() { + return IS_JAVA_12; + } + + /** + * 匹配当前Java的版本。 + * + * @param versionPrefix Java版本前缀 + * + * @return 如果版本匹配,则返回true + */ + private final boolean getJavaVersionMatches(String versionPrefix) { + if (JAVA_VERSION == null) { + return false; + } + + return JAVA_VERSION.startsWith(versionPrefix); + } + + /** + * 判定当前Java的版本是否大于等于指定的版本号。 + * + *

+ * 例如: + * + * + *

    + *
  • 测试JDK 1.2:isJavaVersionAtLeast(1.2f)
  • + *
  • 测试JDK 1.2.1:isJavaVersionAtLeast(1.31f)
  • + *
+ * + * + * @param requiredVersion 需要的版本 + * + * @return 如果当前Java版本大于或等于指定的版本,则返回true + */ + public final boolean isJavaVersionAtLeast(float requiredVersion) { + return getVersionFloat() >= requiredVersion; + } + + /** + * 判定当前Java的版本是否大于等于指定的版本号。 + * + *

+ * 例如: + * + * + *

    + *
  • 测试JDK 1.2:isJavaVersionAtLeast(120)
  • + *
  • 测试JDK 1.2.1:isJavaVersionAtLeast(131)
  • + *
+ * + * + * @param requiredVersion 需要的版本 + * + * @return 如果当前Java版本大于或等于指定的版本,则返回true + */ + public final boolean isJavaVersionAtLeast(int requiredVersion) { + return getVersionInt() >= requiredVersion; + } + + /** + * 将Java Implementation的信息转换成字符串。 + * + * @return JVM impl.的字符串表示 + */ + @Override + public final String toString() { + StringBuilder builder = new StringBuilder(); + + SystemUtil.append(builder, "Java Version: ", getVersion()); + SystemUtil.append(builder, "Java Vendor: ", getVendor()); + SystemUtil.append(builder, "Java Vendor URL: ", getVendorURL()); + + return builder.toString(); + } + +} diff --git a/hutool-system/src/main/java/cn/hutool/system/JavaRuntimeInfo.java b/hutool-system/src/main/java/cn/hutool/system/JavaRuntimeInfo.java new file mode 100644 index 000000000..802770f0b --- /dev/null +++ b/hutool-system/src/main/java/cn/hutool/system/JavaRuntimeInfo.java @@ -0,0 +1,225 @@ +package cn.hutool.system; + +import java.io.Serializable; + +import cn.hutool.core.util.StrUtil; + +/** + * 代表当前运行的JRE的信息。 + */ +public class JavaRuntimeInfo implements Serializable{ + private static final long serialVersionUID = 1L; + + private final String JAVA_RUNTIME_NAME = SystemUtil.get("java.runtime.name", false); + private final String JAVA_RUNTIME_VERSION = SystemUtil.get("java.runtime.version", false); + private final String JAVA_HOME = SystemUtil.get("java.home", false); + private final String JAVA_EXT_DIRS = SystemUtil.get("java.ext.dirs", false); + private final String JAVA_ENDORSED_DIRS = SystemUtil.get("java.endorsed.dirs", false); + private final String JAVA_CLASS_PATH = SystemUtil.get("java.class.path", false); + private final String JAVA_CLASS_VERSION = SystemUtil.get("java.class.version", false); + private final String JAVA_LIBRARY_PATH = SystemUtil.get("java.library.path", false); + + private final String SUN_BOOT_CLASS_PATH = SystemUtil.get("sun.boot.class.path", false); + + private final String SUN_ARCH_DATA_MODEL = SystemUtil.get("sun.arch.data.model", false); + + public final String getSunBoothClassPath() { + return SUN_BOOT_CLASS_PATH; + } + + /** + * JVM is 32M or 64M + * + * @return 32 or 64 + */ + public final String getSunArchDataModel() { + return SUN_ARCH_DATA_MODEL; + } + + /** + * 取得当前JRE的名称(取自系统属性:java.runtime.name)。 + * + *

+ * 例如Sun JDK 1.4.2: "Java(TM) 2 Runtime Environment, Standard Edition" + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + * @since Java 1.3 + */ + public final String getName() { + return JAVA_RUNTIME_NAME; + } + + /** + * 取得当前JRE的版本(取自系统属性:java.runtime.version)。 + * + *

+ * 例如Sun JDK 1.4.2:"1.4.2-b28" + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + * @since Java 1.3 + */ + public final String getVersion() { + return JAVA_RUNTIME_VERSION; + } + + /** + * 取得当前JRE的安装目录(取自系统属性:java.home)。 + * + *

+ * 例如Sun JDK 1.4.2:"/opt/jdk1.4.2/jre" + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + * @since Java 1.1 + */ + public final String getHomeDir() { + return JAVA_HOME; + } + + /** + * 取得当前JRE的扩展目录列表(取自系统属性:java.ext.dirs)。 + * + *

+ * 例如Sun JDK 1.4.2:"/opt/jdk1.4.2/jre/lib/ext:..." + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + * @since Java 1.3 + */ + public final String getExtDirs() { + return JAVA_EXT_DIRS; + } + + /** + * 取得当前JRE的endorsed目录列表(取自系统属性:java.endorsed.dirs)。 + * + *

+ * 例如Sun JDK 1.4.2:"/opt/jdk1.4.2/jre/lib/endorsed:..." + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + * @since Java 1.4 + */ + public final String getEndorsedDirs() { + return JAVA_ENDORSED_DIRS; + } + + /** + * 取得当前JRE的系统classpath(取自系统属性:java.class.path)。 + * + *

+ * 例如:"/home/admin/myclasses:/home/admin/..." + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + * @since Java 1.1 + */ + public final String getClassPath() { + return JAVA_CLASS_PATH; + } + + /** + * 取得当前JRE的系统classpath(取自系统属性:java.class.path)。 + * + *

+ * 例如:"/home/admin/myclasses:/home/admin/..." + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + * @since Java 1.1 + */ + public final String[] getClassPathArray() { + return StrUtil.split(getClassPath(), SystemUtil.get("path.separator", false)); + } + + /** + * 取得当前JRE的class文件格式的版本(取自系统属性:java.class.version)。 + * + *

+ * 例如Sun JDK 1.4.2:"48.0" + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + * @since Java 1.1 + */ + public final String getClassVersion() { + return JAVA_CLASS_VERSION; + } + + /** + * 取得当前JRE的library搜索路径(取自系统属性:java.library.path)。 + * + *

+ * 例如Sun JDK 1.4.2:"/opt/jdk1.4.2/bin:..." + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + */ + public final String getLibraryPath() { + return JAVA_LIBRARY_PATH; + } + + /** + * 取得当前JRE的library搜索路径(取自系统属性:java.library.path)。 + * + *

+ * 例如Sun JDK 1.4.2:"/opt/jdk1.4.2/bin:..." + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + * + */ + public final String[] getLibraryPathArray() { + return StrUtil.split(getLibraryPath(), SystemUtil.get("path.separator", false)); + } + + /** + * 取得当前JRE的URL协议packages列表(取自系统属性:java.library.path)。 + * + *

+ * 例如Sun JDK 1.4.2:"sun.net.www.protocol|..." + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + * + */ + public final String getProtocolPackages() { + return SystemUtil.get("java.protocol.handler.pkgs", true); + } + + /** + * 将当前运行的JRE信息转换成字符串。 + * + * @return JRE信息的字符串表示 + */ + @Override + public final String toString() { + StringBuilder builder = new StringBuilder(); + + SystemUtil.append(builder, "Java Runtime Name: ", getName()); + SystemUtil.append(builder, "Java Runtime Version: ", getVersion()); + SystemUtil.append(builder, "Java Home Dir: ", getHomeDir()); + SystemUtil.append(builder, "Java Extension Dirs: ", getExtDirs()); + SystemUtil.append(builder, "Java Endorsed Dirs: ", getEndorsedDirs()); + SystemUtil.append(builder, "Java Class Path: ", getClassPath()); + SystemUtil.append(builder, "Java Class Version: ", getClassVersion()); + SystemUtil.append(builder, "Java Library Path: ", getLibraryPath()); + SystemUtil.append(builder, "Java Protocol Packages: ", getProtocolPackages()); + + return builder.toString(); + } + +} diff --git a/hutool-system/src/main/java/cn/hutool/system/JavaSpecInfo.java b/hutool-system/src/main/java/cn/hutool/system/JavaSpecInfo.java new file mode 100644 index 000000000..06005cc74 --- /dev/null +++ b/hutool-system/src/main/java/cn/hutool/system/JavaSpecInfo.java @@ -0,0 +1,74 @@ +package cn.hutool.system; + +import java.io.Serializable; + +/** + * 代表Java Specification的信息。 + */ +public class JavaSpecInfo implements Serializable{ + private static final long serialVersionUID = 1L; + + private final String JAVA_SPECIFICATION_NAME = SystemUtil.get("java.specification.name", false); + private final String JAVA_SPECIFICATION_VERSION = SystemUtil.get("java.specification.version", false); + private final String JAVA_SPECIFICATION_VENDOR = SystemUtil.get("java.specification.vendor", false); + + /** + * 取得当前Java Spec.的名称(取自系统属性:java.specification.name)。 + * + *

+ * 例如Sun JDK 1.4.2:"Java Platform API Specification" + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + */ + public final String getName() { + return JAVA_SPECIFICATION_NAME; + } + + /** + * 取得当前Java Spec.的版本(取自系统属性:java.specification.version)。 + * + *

+ * 例如Sun JDK 1.4.2:"1.4" + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + * @since Java 1.3 + */ + public final String getVersion() { + return JAVA_SPECIFICATION_VERSION; + } + + /** + * 取得当前Java Spec.的厂商(取自系统属性:java.specification.vendor)。 + * + *

+ * 例如Sun JDK 1.4.2:"Sun Microsystems Inc." + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + */ + public final String getVendor() { + return JAVA_SPECIFICATION_VENDOR; + } + + /** + * 将Java Specification的信息转换成字符串。 + * + * @return JVM spec.的字符串表示 + */ + @Override + public final String toString() { + StringBuilder builder = new StringBuilder(); + + SystemUtil.append(builder, "Java Spec. Name: ", getName()); + SystemUtil.append(builder, "Java Spec. Version: ", getVersion()); + SystemUtil.append(builder, "Java Spec. Vendor: ", getVendor()); + + return builder.toString(); + } + +} diff --git a/hutool-system/src/main/java/cn/hutool/system/JvmInfo.java b/hutool-system/src/main/java/cn/hutool/system/JvmInfo.java new file mode 100644 index 000000000..c323ace19 --- /dev/null +++ b/hutool-system/src/main/java/cn/hutool/system/JvmInfo.java @@ -0,0 +1,89 @@ +package cn.hutool.system; + +import java.io.Serializable; + +/** + * 代表Java Virtual Machine Implementation的信息。 + */ +public class JvmInfo implements Serializable{ + private static final long serialVersionUID = 1L; + + private final String JAVA_VM_NAME = SystemUtil.get("java.vm.name", false); + private final String JAVA_VM_VERSION = SystemUtil.get("java.vm.version", false); + private final String JAVA_VM_VENDOR = SystemUtil.get("java.vm.vendor", false); + private final String JAVA_VM_INFO = SystemUtil.get("java.vm.info", false); + + /** + * 取得当前JVM impl.的名称(取自系统属性:java.vm.name)。 + * + *

+ * 例如Sun JDK 1.4.2:"Java HotSpot(TM) Client VM" + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + */ + public final String getName() { + return JAVA_VM_NAME; + } + + /** + * 取得当前JVM impl.的版本(取自系统属性:java.vm.version)。 + * + *

+ * 例如Sun JDK 1.4.2:"1.4.2-b28" + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + */ + public final String getVersion() { + return JAVA_VM_VERSION; + } + + /** + * 取得当前JVM impl.的厂商(取自系统属性:java.vm.vendor)。 + * + *

+ * 例如Sun JDK 1.4.2:"Sun Microsystems Inc." + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + */ + public final String getVendor() { + return JAVA_VM_VENDOR; + } + + /** + * 取得当前JVM impl.的信息(取自系统属性:java.vm.info)。 + * + *

+ * 例如Sun JDK 1.4.2:"mixed mode" + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + */ + public final String getInfo() { + return JAVA_VM_INFO; + } + + /** + * 将Java Virutal Machine Implementation的信息转换成字符串。 + * + * @return JVM impl.的字符串表示 + */ + @Override + public final String toString() { + StringBuilder builder = new StringBuilder(); + + SystemUtil.append(builder, "JavaVM Name: ", getName()); + SystemUtil.append(builder, "JavaVM Version: ", getVersion()); + SystemUtil.append(builder, "JavaVM Vendor: ", getVendor()); + SystemUtil.append(builder, "JavaVM Info: ", getInfo()); + + return builder.toString(); + } + +} diff --git a/hutool-system/src/main/java/cn/hutool/system/JvmSpecInfo.java b/hutool-system/src/main/java/cn/hutool/system/JvmSpecInfo.java new file mode 100644 index 000000000..55dbfe90c --- /dev/null +++ b/hutool-system/src/main/java/cn/hutool/system/JvmSpecInfo.java @@ -0,0 +1,73 @@ +package cn.hutool.system; + +import java.io.Serializable; + +/** + * 代表Java Virutal Machine Specification的信息。 + */ +public class JvmSpecInfo implements Serializable{ + private static final long serialVersionUID = 1L; + + private final String JAVA_VM_SPECIFICATION_NAME = SystemUtil.get("java.vm.specification.name", false); + private final String JAVA_VM_SPECIFICATION_VERSION = SystemUtil.get("java.vm.specification.version", false); + private final String JAVA_VM_SPECIFICATION_VENDOR = SystemUtil.get("java.vm.specification.vendor", false); + + /** + * 取得当前JVM spec.的名称(取自系统属性:java.vm.specification.name)。 + * + *

+ * 例如Sun JDK 1.4.2:"Java Virtual Machine Specification" + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + */ + public final String getName() { + return JAVA_VM_SPECIFICATION_NAME; + } + + /** + * 取得当前JVM spec.的版本(取自系统属性:java.vm.specification.version)。 + * + *

+ * 例如Sun JDK 1.4.2:"1.0" + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + */ + public final String getVersion() { + return JAVA_VM_SPECIFICATION_VERSION; + } + + /** + * 取得当前JVM spec.的厂商(取自系统属性:java.vm.specification.vendor)。 + * + *

+ * 例如Sun JDK 1.4.2:"Sun Microsystems Inc." + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + */ + public final String getVendor() { + return JAVA_VM_SPECIFICATION_VENDOR; + } + + /** + * 将Java Virutal Machine Specification的信息转换成字符串。 + * + * @return JVM spec.的字符串表示 + */ + @Override + public final String toString() { + StringBuilder builder = new StringBuilder(); + + SystemUtil.append(builder, "JavaVM Spec. Name: ", getName()); + SystemUtil.append(builder, "JavaVM Spec. Version: ", getVersion()); + SystemUtil.append(builder, "JavaVM Spec. Vendor: ", getVendor()); + + return builder.toString(); + } + +} diff --git a/hutool-system/src/main/java/cn/hutool/system/OsInfo.java b/hutool-system/src/main/java/cn/hutool/system/OsInfo.java new file mode 100644 index 000000000..6bc7ea340 --- /dev/null +++ b/hutool-system/src/main/java/cn/hutool/system/OsInfo.java @@ -0,0 +1,441 @@ +package cn.hutool.system; + +import java.io.Serializable; + +/** + * 代表当前OS的信息。 + */ +public class OsInfo implements Serializable{ + private static final long serialVersionUID = 1L; + + private final String OS_VERSION = SystemUtil.get("os.version", false); + private final String OS_ARCH = SystemUtil.get("os.arch", false); + private final String OS_NAME = SystemUtil.get("os.name", false); + private final boolean IS_OS_AIX = getOSMatches("AIX"); + private final boolean IS_OS_HP_UX = getOSMatches("HP-UX"); + private final boolean IS_OS_IRIX = getOSMatches("Irix"); + private final boolean IS_OS_LINUX = getOSMatches("Linux") || getOSMatches("LINUX"); + private final boolean IS_OS_MAC = getOSMatches("Mac"); + private final boolean IS_OS_MAC_OSX = getOSMatches("Mac OS X"); + private final boolean IS_OS_OS2 = getOSMatches("OS/2"); + private final boolean IS_OS_SOLARIS = getOSMatches("Solaris"); + private final boolean IS_OS_SUN_OS = getOSMatches("SunOS"); + private final boolean IS_OS_WINDOWS = getOSMatches("Windows"); + private final boolean IS_OS_WINDOWS_2000 = getOSMatches("Windows", "5.0"); + private final boolean IS_OS_WINDOWS_95 = getOSMatches("Windows 9", "4.0"); + private final boolean IS_OS_WINDOWS_98 = getOSMatches("Windows 9", "4.1"); + private final boolean IS_OS_WINDOWS_ME = getOSMatches("Windows", "4.9"); + private final boolean IS_OS_WINDOWS_NT = getOSMatches("Windows NT"); + private final boolean IS_OS_WINDOWS_XP = getOSMatches("Windows", "5.1"); + + private final boolean IS_OS_WINDOWS_7 = getOSMatches("Windows", "6.1"); + private final boolean IS_OS_WINDOWS_8 = getOSMatches("Windows", "6.2"); + private final boolean IS_OS_WINDOWS_8_1 = getOSMatches("Windows", "6.3"); + private final boolean IS_OS_WINDOWS_10 = getOSMatches("Windows", "10.0"); + + // 由于改变file.encoding属性并不会改变系统字符编码,为了保持一致,通过LocaleUtil取系统默认编码。 + private final String FILE_SEPARATOR = SystemUtil.get("file.separator", false); + private final String LINE_SEPARATOR = SystemUtil.get("line.separator", false); + private final String PATH_SEPARATOR = SystemUtil.get("path.separator", false); + + /** + * 取得当前OS的架构(取自系统属性:os.arch)。 + * + *

+ * 例如:"x86" + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + * @since Java 1.1 + */ + public final String getArch() { + return OS_ARCH; + } + + /** + * 取得当前OS的名称(取自系统属性:os.name)。 + * + *

+ * 例如:"Windows XP" + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + * @since Java 1.1 + */ + public final String getName() { + return OS_NAME; + } + + /** + * 取得当前OS的版本(取自系统属性:os.version)。 + * + *

+ * 例如:"5.1" + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + * @since Java 1.1 + */ + public final String getVersion() { + return OS_VERSION; + } + + /** + * 判断当前OS的类型。 + * + *

+ * 如果不能取得系统属性os.name(因为Java安全限制),则总是返回false + *

+ * + * @return 如果当前OS类型为AIX,则返回true + */ + public final boolean isAix() { + return IS_OS_AIX; + } + + /** + * 判断当前OS的类型。 + * + *

+ * 如果不能取得系统属性os.name(因为Java安全限制),则总是返回false + *

+ * + * @return 如果当前OS类型为HP-UX,则返回true + */ + public final boolean isHpUx() { + return IS_OS_HP_UX; + } + + /** + * 判断当前OS的类型。 + * + *

+ * 如果不能取得系统属性os.name(因为Java安全限制),则总是返回false + *

+ * + * @return 如果当前OS类型为IRIX,则返回true + */ + public final boolean isIrix() { + return IS_OS_IRIX; + } + + /** + * 判断当前OS的类型。 + * + *

+ * 如果不能取得系统属性os.name(因为Java安全限制),则总是返回false + *

+ * + * @return 如果当前OS类型为Linux,则返回true + */ + public final boolean isLinux() { + return IS_OS_LINUX; + } + + /** + * 判断当前OS的类型。 + * + *

+ * 如果不能取得系统属性os.name(因为Java安全限制),则总是返回false + *

+ * + * @return 如果当前OS类型为Mac,则返回true + */ + public final boolean isMac() { + return IS_OS_MAC; + } + + /** + * 判断当前OS的类型。 + * + *

+ * 如果不能取得系统属性os.name(因为Java安全限制),则总是返回false + *

+ * + * @return 如果当前OS类型为MacOS X,则返回true + */ + public final boolean isMacOsX() { + return IS_OS_MAC_OSX; + } + + /** + * 判断当前OS的类型。 + * + *

+ * 如果不能取得系统属性os.name(因为Java安全限制),则总是返回false + *

+ * + * @return 如果当前OS类型为OS2,则返回true + */ + public final boolean isOs2() { + return IS_OS_OS2; + } + + /** + * 判断当前OS的类型。 + * + *

+ * 如果不能取得系统属性os.name(因为Java安全限制),则总是返回false + *

+ * + * @return 如果当前OS类型为Solaris,则返回true + */ + public final boolean isSolaris() { + return IS_OS_SOLARIS; + } + + /** + * 判断当前OS的类型。 + * + *

+ * 如果不能取得系统属性os.name(因为Java安全限制),则总是返回false + *

+ * + * @return 如果当前OS类型为Sun OS,则返回true + */ + public final boolean isSunOS() { + return IS_OS_SUN_OS; + } + + /** + * 判断当前OS的类型。 + * + *

+ * 如果不能取得系统属性os.name(因为Java安全限制),则总是返回false + *

+ * + * @return 如果当前OS类型为Windows,则返回true + */ + public final boolean isWindows() { + return IS_OS_WINDOWS; + } + + /** + * 判断当前OS的类型。 + * + *

+ * 如果不能取得系统属性os.name(因为Java安全限制),则总是返回false + *

+ * + * @return 如果当前OS类型为Windows 2000,则返回true + */ + public final boolean isWindows2000() { + return IS_OS_WINDOWS_2000; + } + + /** + * 判断当前OS的类型。 + * + *

+ * 如果不能取得系统属性os.name(因为Java安全限制),则总是返回false + *

+ * + * @return 如果当前OS类型为Windows 95,则返回true + */ + public final boolean isWindows95() { + return IS_OS_WINDOWS_95; + } + + /** + * 判断当前OS的类型。 + * + *

+ * 如果不能取得系统属性os.name(因为Java安全限制),则总是返回false + *

+ * + * @return 如果当前OS类型为Windows 98,则返回true + */ + public final boolean isWindows98() { + return IS_OS_WINDOWS_98; + } + + /** + * 判断当前OS的类型。 + * + *

+ * 如果不能取得系统属性os.name(因为Java安全限制),则总是返回false + *

+ * + * @return 如果当前OS类型为Windows ME,则返回true + */ + public final boolean isWindowsME() { + return IS_OS_WINDOWS_ME; + } + + /** + * 判断当前OS的类型。 + * + *

+ * 如果不能取得系统属性os.name(因为Java安全限制),则总是返回false + *

+ * + * @return 如果当前OS类型为Windows NT,则返回true + */ + public final boolean isWindowsNT() { + return IS_OS_WINDOWS_NT; + } + + /** + * 判断当前OS的类型。 + * + *

+ * 如果不能取得系统属性os.name(因为Java安全限制),则总是返回false + *

+ * + * @return 如果当前OS类型为Windows XP,则返回true + */ + public final boolean isWindowsXP() { + return IS_OS_WINDOWS_XP; + } + + /** + * 判断当前OS的类型。 + * + *

+ * 如果不能取得系统属性os.name(因为Java安全限制),则总是返回false + *

+ * + * @return 如果当前OS类型为Windows 7,则返回true + */ + public final boolean isWindows7() { + return IS_OS_WINDOWS_7; + } + + /** + * 判断当前OS的类型。 + * + *

+ * 如果不能取得系统属性os.name(因为Java安全限制),则总是返回false + *

+ * + * @return 如果当前OS类型为Windows 8,则返回true + */ + public final boolean isWindoows8() { + return IS_OS_WINDOWS_8; + } + + /** + * 判断当前OS的类型。 + * + *

+ * 如果不能取得系统属性os.name(因为Java安全限制),则总是返回false + *

+ * + * @return 如果当前OS类型为Windows 8.1,则返回true + */ + public final boolean isWindows8_1() { + return IS_OS_WINDOWS_8_1; + } + + /** + * 判断当前OS的类型。 + * + *

+ * 如果不能取得系统属性os.name(因为Java安全限制),则总是返回false + *

+ * + * @return 如果当前OS类型为Windows 10,则返回true + */ + public final boolean isWindows10() { + return IS_OS_WINDOWS_10; + } + + /** + * 匹配OS名称。 + * + * @param osNamePrefix OS名称前缀 + * + * @return 如果匹配,则返回true + */ + private final boolean getOSMatches(String osNamePrefix) { + if (OS_NAME == null) { + return false; + } + + return OS_NAME.startsWith(osNamePrefix); + } + + /** + * 匹配OS名称。 + * + * @param osNamePrefix OS名称前缀 + * @param osVersionPrefix OS版本前缀 + * + * @return 如果匹配,则返回true + */ + private final boolean getOSMatches(String osNamePrefix, String osVersionPrefix) { + if ((OS_NAME == null) || (OS_VERSION == null)) { + return false; + } + + return OS_NAME.startsWith(osNamePrefix) && OS_VERSION.startsWith(osVersionPrefix); + } + + /** + * 取得OS的文件路径的分隔符(取自系统属性:file.separator)。 + * + *

+ * 例如:Unix为"/",Windows为"\\"。 + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + * @since Java 1.1 + */ + public final String getFileSeparator() { + return FILE_SEPARATOR; + } + + /** + * 取得OS的文本文件换行符(取自系统属性:line.separator)。 + * + *

+ * 例如:Unix为"\n",Windows为"\r\n"。 + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + * @since Java 1.1 + */ + public final String getLineSeparator() { + return LINE_SEPARATOR; + } + + /** + * 取得OS的搜索路径分隔符(取自系统属性:path.separator)。 + * + *

+ * 例如:Unix为":",Windows为";"。 + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + * @since Java 1.1 + */ + public final String getPathSeparator() { + return PATH_SEPARATOR; + } + + /** + * 将OS的信息转换成字符串。 + * + * @return OS的字符串表示 + */ + @Override + public final String toString() { + StringBuilder builder = new StringBuilder(); + + SystemUtil.append(builder, "OS Arch: ", getArch()); + SystemUtil.append(builder, "OS Name: ", getName()); + SystemUtil.append(builder, "OS Version: ", getVersion()); + SystemUtil.append(builder, "File Separator: ", getFileSeparator()); + SystemUtil.append(builder, "Line Separator: ", getLineSeparator()); + SystemUtil.append(builder, "Path Separator: ", getPathSeparator()); + + return builder.toString(); + } + +} diff --git a/hutool-system/src/main/java/cn/hutool/system/RuntimeInfo.java b/hutool-system/src/main/java/cn/hutool/system/RuntimeInfo.java new file mode 100644 index 000000000..63c40b427 --- /dev/null +++ b/hutool-system/src/main/java/cn/hutool/system/RuntimeInfo.java @@ -0,0 +1,68 @@ +package cn.hutool.system; + +import java.io.Serializable; + +import cn.hutool.core.io.FileUtil; + +/** + * 运行时信息,包括内存总大小、已用大小、可用大小等 + * @author looly + * + */ +public class RuntimeInfo implements Serializable{ + private static final long serialVersionUID = 1L; + + private Runtime currentRuntime = Runtime.getRuntime(); + + /** + * 获得运行时对象 + * @return {@link Runtime} + */ + public final Runtime getRuntime(){ + return currentRuntime; + } + + /** + * 获得JVM最大可用内存 + * @return 最大可用内存 + */ + public final long getMaxMemory(){ + return currentRuntime.maxMemory(); + } + + /** + * 获得JVM已分配内存 + * @return 已分配内存 + */ + public final long getTotalMemory(){ + return currentRuntime.totalMemory(); + } + + /** + * 获得JVM已分配内存中的剩余空间 + * @return 已分配内存中的剩余空间 + */ + public final long getFreeMemory(){ + return currentRuntime.freeMemory(); + } + + /** + * 获得JVM最大可用内存 + * @return 最大可用内存 + */ + public final long getUsableMemory(){ + return currentRuntime.maxMemory() - currentRuntime.totalMemory() + currentRuntime.freeMemory(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + + SystemUtil.append(builder, "Max Memory: ", FileUtil.readableFileSize(getMaxMemory())); + SystemUtil.append(builder, "Total Memory: ", FileUtil.readableFileSize(getTotalMemory())); + SystemUtil.append(builder, "Free Memory: ", FileUtil.readableFileSize(getFreeMemory())); + SystemUtil.append(builder, "Usable Memory: ", FileUtil.readableFileSize(getUsableMemory())); + + return builder.toString(); + } +} diff --git a/hutool-system/src/main/java/cn/hutool/system/SystemUtil.java b/hutool-system/src/main/java/cn/hutool/system/SystemUtil.java new file mode 100644 index 000000000..1494e4bc5 --- /dev/null +++ b/hutool-system/src/main/java/cn/hutool/system/SystemUtil.java @@ -0,0 +1,472 @@ +package cn.hutool.system; + +import java.io.PrintWriter; +import java.lang.management.ClassLoadingMXBean; +import java.lang.management.CompilationMXBean; +import java.lang.management.GarbageCollectorMXBean; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.lang.management.MemoryManagerMXBean; +import java.lang.management.MemoryPoolMXBean; +import java.lang.management.OperatingSystemMXBean; +import java.lang.management.RuntimeMXBean; +import java.lang.management.ThreadMXBean; +import java.util.List; +import java.util.Properties; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Console; +import cn.hutool.core.lang.Singleton; +import cn.hutool.core.util.StrUtil; + +/** + * Java的System类封装工具类。
+ * http://blog.csdn.net/zhongweijian/article/details/7619383 + * + * @author Looly + * + */ +public class SystemUtil { + + /***** Java运行时环境信息 *****/ + // Java 运行时环境规范名称 + public final static String SPECIFICATION_NAME = "java.specification.name"; + // Java 运行时环境版本 + public final static String VERSION = "java.version"; + // Java 运行时环境规范版本 + public final static String SPECIFICATION_VERSION = "java.specification.version"; + // Java 运行时环境供应商 + public final static String VENDOR = "java.vendor"; + // Java 运行时环境规范供应商 + public final static String SPECIFICATION_VENDOR = "java.specification.vendor"; + // Java 供应商的 URL + public final static String VENDOR_URL = "java.vendor.url"; + // Java 安装目录 + public final static String HOME = "java.home"; + // 加载库时搜索的路径列表 + public final static String LIBRARY_PATH = "java.library.path"; + // 默认的临时文件路径 + public final static String TMPDIR = "java.io.tmpdir"; + // 要使用的 JIT 编译器的名称 + public final static String COMPILER = "java.compiler"; + // 一个或多个扩展目录的路径 + public final static String EXT_DIRS = "java.ext.dirs"; + + /***** Java虚拟机信息 *****/ + // Java 虚拟机实现名称 + public final static String VM_NAME = "java.vm.name"; + // Java 虚拟机规范名称 + public final static String VM_SPECIFICATION_NAME = "java.vm.specification.name"; + // Java 虚拟机实现版本 + public final static String VM_VERSION = "java.vm.version"; + // Java 虚拟机规范版本 + public final static String VM_SPECIFICATION_VERSION = "java.vm.specification.version"; + // Java 虚拟机实现供应商 + public final static String VM_VENDEOR = "java.vm.vendor"; + // Java 虚拟机规范供应商 + public final static String VM_SPECIFICATION_VENDOR = "java.vm.specification.vendor"; + + /***** Java类信息 *****/ + // Java 类格式版本号 + public final static String CLASS_VERSION = "java.class.version"; + // Java 类路径 + public final static String CLASS_PATH = "java.class.path"; + + /***** OS信息 *****/ + // 操作系统的名称 + public final static String OS_NAME = "os.name"; + // 操作系统的架构 + public final static String OS_ARCH = "os.arch"; + // 操作系统的版本 + public final static String OS_VERSION = "os.version"; + // 文件分隔符(在 UNIX 系统中是“/”) + public final static String FILE_SEPRATOR = "file.separator"; + // 路径分隔符(在 UNIX 系统中是“:”) + public final static String PATH_SEPRATOR = "path.separator"; + // 行分隔符(在 UNIX 系统中是“\n”) + public final static String LINE_SEPRATOR = "line.separator"; + + /***** 用户信息 *****/ + // 用户的账户名称 + public final static String USER_NAME = "user.name"; + // 用户的主目录 + public final static String USER_HOME = "user.home"; + // 用户的当前工作目录 + public final static String USER_DIR = "user.dir"; + + // ----------------------------------------------------------------------- Basic start + + /** + * 取得系统属性,如果因为Java安全的限制而失败,则将错误打在Log中,然后返回 null。 + * + * @param name 属性名 + * @param defaultValue 默认值 + * @return 属性值或null + */ + public static String get(String name, String defaultValue) { + return StrUtil.nullToDefault(get(name, false), defaultValue); + } + + /** + * 取得系统属性,如果因为Java安全的限制而失败,则将错误打在Log中,然后返回 null。 + * + * @param name 属性名 + * @param quiet 安静模式,不将出错信息打在System.err中 + * + * @return 属性值或null + */ + public static String get(String name, boolean quiet) { + try { + return System.getProperty(name); + } catch (SecurityException e) { + if (false == quiet) { + Console.error("Caught a SecurityException reading the system property '{}'; the SystemUtil property value will default to null.", name); + } + return null; + } + } + + /** + * 获得System属性(调用System.getProperty) + * + * @param key 键 + * @return 属性值 + */ + 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; + } + + value = value.trim().toLowerCase(); + if (value.isEmpty()) { + return true; + } + + if ("true".equals(value) || "yes".equals(value) || "1".equals(value)) { + return true; + } + + if ("false".equals(value) || "no".equals(value) || "0".equals(value)) { + return false; + } + + return defaultValue; + } + + /** + * 获得int类型值 + * + * @param key 键 + * @param defaultValue 默认值 + * @return 值 + */ + public static long 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 props() { + return System.getProperties(); + } + + /** + * 获取当前进程 PID + * + * @return 当前进程 ID + */ + public static long getCurrentPID() { + return Long.parseLong(getRuntimeMXBean().getName().split("@")[0]); + } + // ----------------------------------------------------------------------- Basic end + + /** + * 返回Java虚拟机类加载系统相关属性 + * + * @return {@link ClassLoadingMXBean} + * @since 4.1.4 + */ + public static ClassLoadingMXBean getClassLoadingMXBean() { + return ManagementFactory.getClassLoadingMXBean(); + } + + /** + * 返回Java虚拟机内存系统相关属性 + * + * @return {@link MemoryMXBean} + * @since 4.1.4 + */ + public static MemoryMXBean getMemoryMXBean() { + return ManagementFactory.getMemoryMXBean(); + } + + /** + * 返回Java虚拟机线程系统相关属性 + * + * @return {@link ThreadMXBean} + * @since 4.1.4 + */ + public static ThreadMXBean getThreadMXBean() { + return ManagementFactory.getThreadMXBean(); + } + + /** + * 返回Java虚拟机运行时系统相关属性 + * + * @return {@link RuntimeMXBean} + * @since 4.1.4 + */ + public static RuntimeMXBean getRuntimeMXBean() { + return ManagementFactory.getRuntimeMXBean(); + } + + /** + * 返回Java虚拟机编译系统相关属性
+ * 如果没有编译系统,则返回null + * + * @return a {@link CompilationMXBean} ,如果没有编译系统,则返回null + * @since 4.1.4 + */ + public static CompilationMXBean getCompilationMXBean() { + return ManagementFactory.getCompilationMXBean(); + } + + /** + * 返回Java虚拟机运行下的操作系统相关信息属性 + * + * @return {@link OperatingSystemMXBean} + * @since 4.1.4 + */ + public static OperatingSystemMXBean getOperatingSystemMXBean() { + return ManagementFactory.getOperatingSystemMXBean(); + } + + /** + * Returns a list of {@link MemoryPoolMXBean} objects in the Java virtual machine.
+ * The Java virtual machine can have one or more memory pools. It may add or remove memory pools during execution. + * + * @return a list of MemoryPoolMXBean objects. + * + */ + public static List getMemoryPoolMXBeans() { + return ManagementFactory.getMemoryPoolMXBeans(); + } + + /** + * Returns a list of {@link MemoryManagerMXBean} objects in the Java virtual machine.
+ * The Java virtual machine can have one or more memory managers. It may add or remove memory managers during execution. + * + * @return a list of MemoryManagerMXBean objects. + * + */ + public static List getMemoryManagerMXBeans() { + return ManagementFactory.getMemoryManagerMXBeans(); + } + + /** + * Returns a list of {@link GarbageCollectorMXBean} objects in the Java virtual machine.
+ * The Java virtual machine may have one or more GarbageCollectorMXBean objects.
+ * It may add or remove GarbageCollectorMXBean during execution. + * + * @return a list of GarbageCollectorMXBean objects. + * + */ + public static List getGarbageCollectorMXBeans() { + return ManagementFactory.getGarbageCollectorMXBeans(); + } + + /** + * 取得Java Virtual Machine Specification的信息。 + * + * @return JvmSpecInfo对象 + */ + public static JvmSpecInfo getJvmSpecInfo() { + return Singleton.get(JvmSpecInfo.class); + } + + /** + * 取得Java Virtual Machine Implementation的信息。 + * + * @return JvmInfo对象 + */ + public static JvmInfo getJvmInfo() { + return Singleton.get(JvmInfo.class); + } + + /** + * 取得Java Specification的信息。 + * + * @return JavaSpecInfo对象 + */ + public static JavaSpecInfo getJavaSpecInfo() { + return Singleton.get(JavaSpecInfo.class); + } + + /** + * 取得Java Implementation的信息。 + * + * @return JavaInfo对象 + */ + public static JavaInfo getJavaInfo() { + return Singleton.get(JavaInfo.class); + } + + /** + * 取得当前运行的JRE的信息。 + * + * @return JreInfo对象 + */ + public static JavaRuntimeInfo getJavaRuntimeInfo() { + return Singleton.get(JavaRuntimeInfo.class); + } + + /** + * 取得OS的信息。 + * + * @return OsInfo对象 + */ + public static OsInfo getOsInfo() { + return Singleton.get(OsInfo.class); + } + + /** + * 取得User的信息。 + * + * @return UserInfo对象 + */ + public static UserInfo getUserInfo() { + return Singleton.get(UserInfo.class); + } + + /** + * 取得Host的信息。 + * + * @return HostInfo对象 + */ + public static HostInfo getHostInfo() { + return Singleton.get(HostInfo.class); + } + + /** + * 取得Runtime的信息。 + * + * @return RuntimeInfo对象 + */ + public static RuntimeInfo getRuntimeInfo() { + return Singleton.get(RuntimeInfo.class); + } + + /** + * 获取JVM中内存总大小 + * + * @return 内存总大小 + * @since 4.5.4 + */ + public static long getTotalMemory() { + return Runtime.getRuntime().totalMemory(); + } + + /** + * 获取JVM中内存剩余大小 + * + * @return 内存剩余大小 + * @since 4.5.4 + */ + public static long getFreeMemory() { + return Runtime.getRuntime().freeMemory(); + } + + /** + * 获取JVM可用的内存总大小 + * + * @return JVM可用的内存总大小 + * @since 4.5.4 + */ + public static long getMaxMemory() { + return Runtime.getRuntime().maxMemory(); + } + + /** + * 获取总线程数 + * + * @return 总线程数 + */ + public static int getTotalThreadCount() { + ThreadGroup parentThread = Thread.currentThread().getThreadGroup(); + while(null != parentThread.getParent()) { + parentThread = parentThread.getParent(); + } + return parentThread.activeCount(); + } + + // ------------------------------------------------------------------ Dump + /** + * 将系统信息输出到System.out中。 + */ + public static void dumpSystemInfo() { + dumpSystemInfo(new PrintWriter(System.out)); + } + + /** + * 将系统信息输出到指定PrintWriter中。 + * + * @param out PrintWriter输出流 + */ + public static void dumpSystemInfo(PrintWriter out) { + out.println("--------------"); + out.println(getJvmSpecInfo()); + out.println("--------------"); + out.println(getJvmInfo()); + out.println("--------------"); + out.println(getJavaSpecInfo()); + out.println("--------------"); + out.println(getJavaInfo()); + out.println("--------------"); + out.println(getJavaRuntimeInfo()); + out.println("--------------"); + out.println(getOsInfo()); + out.println("--------------"); + out.println(getUserInfo()); + out.println("--------------"); + out.println(getHostInfo()); + out.println("--------------"); + out.println(getRuntimeInfo()); + out.println("--------------"); + out.flush(); + } + + /** + * 输出到StringBuilder。 + * + * @param builder StringBuilder对象 + * @param caption 标题 + * @param value 值 + */ + protected static void append(StringBuilder builder, String caption, Object value) { + builder.append(caption).append(StrUtil.nullToDefault(Convert.toStr(value), "[n/a]")).append("\n"); + } +} diff --git a/hutool-system/src/main/java/cn/hutool/system/UserInfo.java b/hutool-system/src/main/java/cn/hutool/system/UserInfo.java new file mode 100644 index 000000000..86fbf60ad --- /dev/null +++ b/hutool-system/src/main/java/cn/hutool/system/UserInfo.java @@ -0,0 +1,125 @@ +package cn.hutool.system; + +import java.io.Serializable; + +/** + * 代表当前用户的信息。 + */ +public class UserInfo implements Serializable{ + private static final long serialVersionUID = 1L; + + private final String USER_NAME = SystemUtil.get("user.name", false); + private final String USER_HOME = SystemUtil.get("user.home", false); + private final String USER_DIR = SystemUtil.get("user.dir", false); + private final String USER_LANGUAGE = SystemUtil.get("user.language", false); + private final String USER_COUNTRY = ((SystemUtil.get("user.country", false) == null) ? SystemUtil.get("user.region", false) : SystemUtil.get("user.country", false)); + private final String JAVA_IO_TMPDIR = SystemUtil.get("java.io.tmpdir", false); + + /** + * 取得当前登录用户的名字(取自系统属性:user.name)。 + * + *

+ * 例如:"admin" + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + * @since Java 1.1 + */ + public final String getName() { + return USER_NAME; + } + + /** + * 取得当前登录用户的home目录(取自系统属性:user.home)。 + * + *

+ * 例如:"/home/admin" + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + * @since Java 1.1 + */ + public final String getHomeDir() { + return USER_HOME; + } + + /** + * 取得当前目录(取自系统属性:user.dir)。 + * + *

+ * 例如:"/home/admin/working" + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + * @since Java 1.1 + */ + public final String getCurrentDir() { + return USER_DIR; + } + + /** + * 取得临时目录(取自系统属性:java.io.tmpdir)。 + * + *

+ * 例如:"/tmp" + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + * + */ + public final String getTempDir() { + return JAVA_IO_TMPDIR; + } + + /** + * 取得当前登录用户的语言设置(取自系统属性:user.language)。 + * + *

+ * 例如:"zh""en"等 + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + */ + public final String getLanguage() { + return USER_LANGUAGE; + } + + /** + * 取得当前登录用户的国家或区域设置(取自系统属性:JDK1.4 user.country或JDK1.2 user.region)。 + * + *

+ * 例如:"CN""US"等 + *

+ * + * @return 属性值,如果不能取得(因为Java安全限制)或值不存在,则返回null。 + * + */ + public final String getCountry() { + return USER_COUNTRY; + } + + /** + * 将当前用户的信息转换成字符串。 + * + * @return 用户信息的字符串表示 + */ + @Override + public final String toString() { + StringBuilder builder = new StringBuilder(); + + SystemUtil.append(builder, "User Name: ", getName()); + SystemUtil.append(builder, "User Home Dir: ", getHomeDir()); + SystemUtil.append(builder, "User Current Dir: ", getCurrentDir()); + SystemUtil.append(builder, "User Temp Dir: ", getTempDir()); + SystemUtil.append(builder, "User Language: ", getLanguage()); + SystemUtil.append(builder, "User Country: ", getCountry()); + + return builder.toString(); + } + +} diff --git a/hutool-system/src/main/java/cn/hutool/system/package-info.java b/hutool-system/src/main/java/cn/hutool/system/package-info.java new file mode 100644 index 000000000..63aa3907d --- /dev/null +++ b/hutool-system/src/main/java/cn/hutool/system/package-info.java @@ -0,0 +1,7 @@ +/** + * System模块主要获取系统、JVM、内存、CPU等信息,以便动态监测系统状态 + * + * @author looly + * + */ +package cn.hutool.system; \ No newline at end of file diff --git a/hutool-system/src/test/java/cn/hutool/system/SystemUtilTest.java b/hutool-system/src/test/java/cn/hutool/system/SystemUtilTest.java new file mode 100644 index 000000000..84741361f --- /dev/null +++ b/hutool-system/src/test/java/cn/hutool/system/SystemUtilTest.java @@ -0,0 +1,33 @@ +package cn.hutool.system; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +public class SystemUtilTest { + + @Test + @Ignore + public void dumpTest() { + SystemUtil.dumpSystemInfo(); + } + + @Test + public void getCurrentPidTest() { + long pid = SystemUtil.getCurrentPID(); + Assert.assertTrue(pid > 0); + } + + @Test + public void getJavaInfoTest() { + JavaInfo javaInfo = SystemUtil.getJavaInfo(); + Assert.assertNotNull(javaInfo); + } + + @Test + public void getOsInfoTest() { + OsInfo osInfo = SystemUtil.getOsInfo(); + Assert.assertNotNull(osInfo); + } + +} diff --git a/hutool.sh b/hutool.sh new file mode 100755 index 000000000..c1dcd396c --- /dev/null +++ b/hutool.sh @@ -0,0 +1,32 @@ +#!/bin/bash + + +# Help info function +help(){ + echo "--------------------------------------------------------------------------" + echo "" + echo "usage: ./hutool.sh [install | doc | pack]" + echo "" + echo "-install Install Hutool to your local Maven repository." + echo "-doc Generate Java doc api for Hutool, you can see it in target dir" + echo "-pack Make jar package by Maven" + echo "" + echo "--------------------------------------------------------------------------" +} + + +# Start +./bin/logo.sh +case "$1" in + 'install') + bin/install.sh + ;; + 'doc') + bin/javadoc.sh + ;; + 'pack') + bin/package.sh + ;; + *) + help +esac diff --git a/pom.xml b/pom.xml new file mode 100644 index 000000000..83ed7e129 --- /dev/null +++ b/pom.xml @@ -0,0 +1,184 @@ + + + 4.0.0 + + pom + + cn.hutool + hutool-parent + 4.6.2-SNAPSHOT + hutool + 提供丰富的Java工具方法 + https://github.com/looly/hutool + + + hutool-all + hutool-bom + hutool-aop + hutool-bloomFilter + hutool-cache + hutool-core + hutool-cron + hutool-crypto + hutool-db + hutool-dfa + hutool-extra + hutool-http + hutool-log + hutool-script + hutool-setting + hutool-system + hutool-json + hutool-poi + hutool-captcha + hutool-socket + + + + utf-8 + utf-8 + + + 7 + 4.12 + + + + + + junit + junit + ${junit.version} + test + + + + + Github Issue + https://github.com/looly/hutool/issues + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + + Looly + loolly@gmail.com + + + + + scm:git@github.com:looly/hutool.git + scm:git@github.com:looly/hutool.git + git@github.com:looly/hutool.git + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${compile.version} + ${compile.version} + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.1.1 + + + package + + jar + + + + + + + + + + release + + + oss + https://oss.sonatype.org/content/repositories/snapshots/ + + + oss + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.1.0 + + + oss + package + + jar-no-fork + + + + + + org.codehaus.mojo + cobertura-maven-plugin + 2.7 + + + html + xml + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + oss + verify + + sign + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.8 + true + + oss + https://oss.sonatype.org/ + true + + + + + + + +