From 2f8051947226b2d74322c7581e5139256d263d24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Berg=20Glasius?= Date: Wed, 22 Apr 2026 14:16:42 +0200 Subject: [PATCH 1/6] Remove Travis CI and GitHub Actions configuration, add .editorconfig file * Made into multi-build project * Fixed tests * Cleaned up Taggable * Cleaned up TagLib descriptor --- .editorconfig | 1393 +++++++++++++++++ .github/workflows/build.yml | 28 - .github/workflows/ci.yml | 110 ++ .github/workflows/release-notes.yml | 26 + .github/workflows/release.yml | 281 +++- .sdkmanrc | 3 + .skills/gradle-best-practices.md | 254 +++ .skills/plugin-project.md | 145 ++ .skills/repository-structure.md | 216 +++ .travis.yml | 19 - AGENTS.md | 202 +++ CLAUDE.md | 1 + CONTRIBUTING.md | 163 ++ README.md | 9 +- build-logic/build.gradle | 29 + build-logic/settings.gradle | 13 + .../src/main/groovy/config.app-run.gradle | 12 + .../src/main/groovy/config.compile.gradle | 65 + .../src/main/groovy/config.docs.gradle | 110 ++ .../src/main/groovy/config.example-app.gradle | 8 + .../main/groovy/config.grails-assets.gradle | 26 + .../main/groovy/config.grails-plugin.gradle | 16 + .../main/groovy/config.publish-root.gradle | 16 + .../src/main/groovy/config.publish.gradle | 9 + .../src/main/groovy/config.testing.gradle | 45 + build.gradle | 191 +-- docs/build.gradle | 41 + docs/src/docs/gettingStarted.adoc | 52 + docs/src/docs/index.adoc | 8 + docs/src/docs/index.tmpl | 389 +++++ docs/src/docs/introduction.adoc | 12 + .../src/docs/introduction/currentVersion.adoc | 14 + docs/src/docs/introduction/license.adoc | 6 + docs/src/docs/introduction/sourceCode.adoc | 12 + docs/src/docs/usage.adoc | 93 ++ examples/app1/build.gradle | 47 + .../assets/stylesheets/application.css | 5 + examples/app1/grails-app/conf/application.yml | 93 ++ .../app1/grails-app}/conf/logback.xml | 0 .../plugins/taggable/TestDescendent.groovy | 0 .../grails/plugins/taggable/TestDomain.groovy | 0 .../plugins/taggable/Application.groovy | 5 +- .../app1/grails-app/views/layouts/main.gsp | 13 + .../plugins/taggable/TaggableSpec.groovy | 283 ++++ gradle.properties | 23 +- gradle/wrapper/gradle-wrapper.jar | Bin 52818 -> 43764 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 309 ++-- gradlew.bat | 90 +- grails-app/conf/application.yml | 110 -- plugin/build.gradle | 51 + plugin/grails-app/assets/images/.gitkeep | 0 plugin/grails-app/conf/application.yml | 10 + .../domain/grails/plugins/taggable/Tag.groovy | 0 .../grails/plugins/taggable/TagLink.groovy | 0 .../plugins/taggable/TaggableService.groovy | 36 +- .../grails/plugins/taggable/TagsTagLib.groovy | 0 .../plugins/taggable/TagException.groovy | 0 .../grails/plugins/taggable/Taggable.groovy | 172 +- .../plugins/taggable/TagsTagLibSpec.groovy | 32 +- settings.gradle | 57 + .../plugins/taggable/TaggableSpec.groovy | 298 ---- travis-build.sh | 22 - 63 files changed, 4633 insertions(+), 1044 deletions(-) create mode 100644 .editorconfig delete mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release-notes.yml create mode 100644 .sdkmanrc create mode 100644 .skills/gradle-best-practices.md create mode 100644 .skills/plugin-project.md create mode 100644 .skills/repository-structure.md delete mode 100644 .travis.yml create mode 100644 AGENTS.md create mode 120000 CLAUDE.md create mode 100644 CONTRIBUTING.md create mode 100644 build-logic/build.gradle create mode 100644 build-logic/settings.gradle create mode 100644 build-logic/src/main/groovy/config.app-run.gradle create mode 100644 build-logic/src/main/groovy/config.compile.gradle create mode 100644 build-logic/src/main/groovy/config.docs.gradle create mode 100644 build-logic/src/main/groovy/config.example-app.gradle create mode 100644 build-logic/src/main/groovy/config.grails-assets.gradle create mode 100644 build-logic/src/main/groovy/config.grails-plugin.gradle create mode 100644 build-logic/src/main/groovy/config.publish-root.gradle create mode 100644 build-logic/src/main/groovy/config.publish.gradle create mode 100644 build-logic/src/main/groovy/config.testing.gradle create mode 100644 docs/build.gradle create mode 100644 docs/src/docs/gettingStarted.adoc create mode 100644 docs/src/docs/index.adoc create mode 100644 docs/src/docs/index.tmpl create mode 100644 docs/src/docs/introduction.adoc create mode 100644 docs/src/docs/introduction/currentVersion.adoc create mode 100644 docs/src/docs/introduction/license.adoc create mode 100644 docs/src/docs/introduction/sourceCode.adoc create mode 100644 docs/src/docs/usage.adoc create mode 100644 examples/app1/build.gradle create mode 100644 examples/app1/grails-app/assets/stylesheets/application.css create mode 100644 examples/app1/grails-app/conf/application.yml rename {grails-app => examples/app1/grails-app}/conf/logback.xml (100%) rename {grails-app => examples/app1/grails-app}/domain/grails/plugins/taggable/TestDescendent.groovy (100%) rename {grails-app => examples/app1/grails-app}/domain/grails/plugins/taggable/TestDomain.groovy (100%) rename {grails-app => examples/app1/grails-app}/init/grails/plugins/taggable/Application.groovy (80%) create mode 100644 examples/app1/grails-app/views/layouts/main.gsp create mode 100644 examples/app1/src/integration-test/groovy/grails/plugins/taggable/TaggableSpec.groovy delete mode 100644 grails-app/conf/application.yml create mode 100644 plugin/build.gradle create mode 100644 plugin/grails-app/assets/images/.gitkeep create mode 100644 plugin/grails-app/conf/application.yml rename {grails-app => plugin/grails-app}/domain/grails/plugins/taggable/Tag.groovy (100%) rename {grails-app => plugin/grails-app}/domain/grails/plugins/taggable/TagLink.groovy (100%) rename {grails-app => plugin/grails-app}/services/grails/plugins/taggable/TaggableService.groovy (51%) rename {grails-app => plugin/grails-app}/taglib/grails/plugins/taggable/TagsTagLib.groovy (100%) rename {src => plugin/src}/main/groovy/grails/plugins/taggable/TagException.groovy (100%) rename {src => plugin/src}/main/groovy/grails/plugins/taggable/Taggable.groovy (54%) rename {src => plugin/src}/test/groovy/grails/plugins/taggable/TagsTagLibSpec.groovy (61%) create mode 100644 settings.gradle delete mode 100644 src/integration-test/groovy/grails/plugins/taggable/TaggableSpec.groovy delete mode 100755 travis-build.sh diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1de0688 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,1393 @@ +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +max_line_length = 120 +tab_width = 4 +ij_continuation_indent_size = 8 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = true +ij_smart_tabs = false +ij_visual_guides = +ij_wrap_on_typing = false + +[*.bazelproject] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 2 +ij_projectview_keep_indents_on_empty_lines = false + +[*.css] +ij_css_align_closing_brace_with_properties = false +ij_css_blank_lines_around_nested_selector = 1 +ij_css_blank_lines_between_blocks = 1 +ij_css_block_comment_add_space = false +ij_css_brace_placement = end_of_line +ij_css_enforce_quotes_on_format = false +ij_css_hex_color_long_format = false +ij_css_hex_color_lower_case = false +ij_css_hex_color_short_format = false +ij_css_hex_color_upper_case = false +ij_css_keep_blank_lines_in_code = 2 +ij_css_keep_indents_on_empty_lines = false +ij_css_keep_single_line_blocks = false +ij_css_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow +ij_css_space_after_colon = true +ij_css_space_before_opening_brace = true +ij_css_use_double_quotes = true +ij_css_value_alignment = do_not_align + +[*.dcl] +ij_declarative_keep_indents_on_empty_lines = false + +[*.gsp] +ij_gsp_keep_indents_on_empty_lines = false + +[*.java] +ij_java_align_consecutive_assignments = false +ij_java_align_consecutive_variable_declarations = false +ij_java_align_group_field_declarations = false +ij_java_align_multiline_annotation_parameters = false +ij_java_align_multiline_array_initializer_expression = false +ij_java_align_multiline_assignment = false +ij_java_align_multiline_binary_operation = false +ij_java_align_multiline_chained_methods = false +ij_java_align_multiline_deconstruction_list_components = true +ij_java_align_multiline_extends_list = false +ij_java_align_multiline_for = true +ij_java_align_multiline_method_parentheses = false +ij_java_align_multiline_parameters = true +ij_java_align_multiline_parameters_in_calls = false +ij_java_align_multiline_parenthesized_expression = false +ij_java_align_multiline_records = true +ij_java_align_multiline_resources = true +ij_java_align_multiline_ternary_operation = false +ij_java_align_multiline_text_blocks = false +ij_java_align_multiline_throws_list = false +ij_java_align_subsequent_simple_methods = false +ij_java_align_throws_keyword = false +ij_java_align_types_in_multi_catch = true +ij_java_annotation_new_line_in_record_component = false +ij_java_annotation_parameter_wrap = off +ij_java_array_initializer_new_line_after_left_brace = false +ij_java_array_initializer_right_brace_on_new_line = false +ij_java_array_initializer_wrap = off +ij_java_assert_statement_colon_on_next_line = false +ij_java_assert_statement_wrap = off +ij_java_assignment_wrap = off +ij_java_binary_operation_sign_on_next_line = false +ij_java_binary_operation_wrap = off +ij_java_blank_lines_after_anonymous_class_header = 0 +ij_java_blank_lines_after_class_header = 0 +ij_java_blank_lines_after_imports = 1 +ij_java_blank_lines_after_package = 1 +ij_java_blank_lines_around_class = 1 +ij_java_blank_lines_around_field = 0 +ij_java_blank_lines_around_field_in_interface = 0 +ij_java_blank_lines_around_field_with_annotations = 0 +ij_java_blank_lines_around_initializer = 1 +ij_java_blank_lines_around_method = 1 +ij_java_blank_lines_around_method_in_interface = 1 +ij_java_blank_lines_before_class_end = 0 +ij_java_blank_lines_before_imports = 1 +ij_java_blank_lines_before_method_body = 0 +ij_java_blank_lines_before_package = 0 +ij_java_blank_lines_between_record_components = 0 +ij_java_block_brace_style = end_of_line +ij_java_block_comment_add_space = false +ij_java_block_comment_at_first_column = true +ij_java_builder_methods = +ij_java_call_parameters_new_line_after_left_paren = false +ij_java_call_parameters_right_paren_on_new_line = false +ij_java_call_parameters_wrap = off +ij_java_case_statement_on_separate_line = true +ij_java_catch_on_new_line = false +ij_java_class_annotation_wrap = split_into_lines +ij_java_class_brace_style = end_of_line +ij_java_class_count_to_use_import_on_demand = 999 +ij_java_class_names_in_javadoc = 1 +ij_java_deconstruction_list_wrap = normal +ij_java_delete_unused_module_imports = false +ij_java_do_not_indent_top_level_class_members = false +ij_java_do_not_wrap_after_single_annotation = false +ij_java_do_not_wrap_after_single_annotation_in_parameter = false +ij_java_do_while_brace_force = never +ij_java_doc_add_blank_line_after_description = true +ij_java_doc_add_blank_line_after_param_comments = false +ij_java_doc_add_blank_line_after_return = false +ij_java_doc_add_p_tag_on_empty_lines = true +ij_java_doc_align_exception_comments = true +ij_java_doc_align_param_comments = true +ij_java_doc_do_not_wrap_if_one_line = false +ij_java_doc_enable_formatting = true +ij_java_doc_enable_leading_asterisks = true +ij_java_doc_indent_on_continuation = false +ij_java_doc_keep_empty_lines = true +ij_java_doc_keep_empty_parameter_tag = true +ij_java_doc_keep_empty_return_tag = true +ij_java_doc_keep_empty_throws_tag = true +ij_java_doc_keep_invalid_tags = true +ij_java_doc_param_description_on_new_line = false +ij_java_doc_preserve_line_breaks = false +ij_java_doc_use_throws_not_exception_tag = true +ij_java_else_on_new_line = false +ij_java_entity_dd_prefix = +ij_java_entity_dd_suffix = EJB +ij_java_entity_eb_prefix = +ij_java_entity_eb_suffix = Bean +ij_java_entity_hi_prefix = +ij_java_entity_hi_suffix = Home +ij_java_entity_lhi_prefix = Local +ij_java_entity_lhi_suffix = Home +ij_java_entity_li_prefix = Local +ij_java_entity_li_suffix = +ij_java_entity_pk_class = java.lang.String +ij_java_entity_ri_prefix = +ij_java_entity_ri_suffix = +ij_java_entity_vo_prefix = +ij_java_entity_vo_suffix = VO +ij_java_enum_constants_wrap = off +ij_java_enum_field_annotation_wrap = off +ij_java_extends_keyword_wrap = off +ij_java_extends_list_wrap = off +ij_java_field_annotation_wrap = split_into_lines +ij_java_field_name_prefix = +ij_java_field_name_suffix = +ij_java_filter_class_prefix = +ij_java_filter_class_suffix = +ij_java_filter_dd_prefix = +ij_java_filter_dd_suffix = +ij_java_finally_on_new_line = false +ij_java_for_brace_force = never +ij_java_for_statement_new_line_after_left_paren = false +ij_java_for_statement_right_paren_on_new_line = false +ij_java_for_statement_wrap = off +ij_java_generate_final_locals = false +ij_java_generate_final_parameters = false +ij_java_generate_use_type_annotation_before_type = true +ij_java_if_brace_force = never +ij_java_imports_layout = java.**, |, javax.**, |, groovy.**, org.apache.groovy.**, org.codehaus.groovy.**, |, jakarta.**, |, *, |, io.spring.**, org.springframework.**, |, grails.**, org.apache.grails.**, org.grails.**, |, $* +ij_java_indent_case_from_switch = true +ij_java_insert_inner_class_imports = false +ij_java_insert_override_annotation = true +ij_java_keep_blank_lines_before_right_brace = 2 +ij_java_keep_blank_lines_between_package_declaration_and_header = 2 +ij_java_keep_blank_lines_in_code = 2 +ij_java_keep_blank_lines_in_declarations = 2 +ij_java_keep_builder_methods_indents = false +ij_java_keep_control_statement_in_one_line = true +ij_java_keep_first_column_comment = true +ij_java_keep_indents_on_empty_lines = false +ij_java_keep_line_breaks = true +ij_java_keep_multiple_expressions_in_one_line = false +ij_java_keep_simple_blocks_in_one_line = false +ij_java_keep_simple_classes_in_one_line = false +ij_java_keep_simple_lambdas_in_one_line = false +ij_java_keep_simple_methods_in_one_line = false +ij_java_label_indent_absolute = false +ij_java_label_indent_size = 0 +ij_java_lambda_brace_style = end_of_line +ij_java_layout_on_demand_import_from_same_package_first = true +ij_java_layout_static_imports_separately = true +ij_java_line_comment_add_space = false +ij_java_line_comment_add_space_on_reformat = false +ij_java_line_comment_at_first_column = true +ij_java_listener_class_prefix = +ij_java_listener_class_suffix = +ij_java_local_variable_name_prefix = +ij_java_local_variable_name_suffix = +ij_java_message_dd_prefix = +ij_java_message_dd_suffix = EJB +ij_java_message_eb_prefix = +ij_java_message_eb_suffix = Bean +ij_java_method_annotation_wrap = split_into_lines +ij_java_method_brace_style = end_of_line +ij_java_method_call_chain_wrap = off +ij_java_method_parameters_new_line_after_left_paren = false +ij_java_method_parameters_right_paren_on_new_line = false +ij_java_method_parameters_wrap = off +ij_java_modifier_list_wrap = false +ij_java_multi_catch_types_wrap = normal +ij_java_names_count_to_use_import_on_demand = 999 +ij_java_new_line_after_lparen_in_annotation = false +ij_java_new_line_after_lparen_in_deconstruction_pattern = true +ij_java_new_line_after_lparen_in_record_header = false +ij_java_new_line_when_body_is_presented = false +ij_java_packages_to_use_import_on_demand = +ij_java_parameter_annotation_wrap = off +ij_java_parameter_name_prefix = +ij_java_parameter_name_suffix = +ij_java_parentheses_expression_new_line_after_left_paren = false +ij_java_parentheses_expression_right_paren_on_new_line = false +ij_java_place_assignment_sign_on_next_line = false +ij_java_prefer_longer_names = true +ij_java_prefer_parameters_wrap = false +ij_java_preserve_module_imports = true +ij_java_record_components_wrap = normal +ij_java_repeat_annotations = +ij_java_repeat_synchronized = true +ij_java_replace_instanceof_and_cast = false +ij_java_replace_null_check = true +ij_java_replace_sum_lambda_with_method_ref = true +ij_java_resource_list_new_line_after_left_paren = false +ij_java_resource_list_right_paren_on_new_line = false +ij_java_resource_list_wrap = off +ij_java_rparen_on_new_line_in_annotation = false +ij_java_rparen_on_new_line_in_deconstruction_pattern = true +ij_java_rparen_on_new_line_in_record_header = false +ij_java_servlet_class_prefix = +ij_java_servlet_class_suffix = +ij_java_servlet_dd_prefix = +ij_java_servlet_dd_suffix = +ij_java_session_dd_prefix = +ij_java_session_dd_suffix = EJB +ij_java_session_eb_prefix = +ij_java_session_eb_suffix = Bean +ij_java_session_hi_prefix = +ij_java_session_hi_suffix = Home +ij_java_session_lhi_prefix = Local +ij_java_session_lhi_suffix = Home +ij_java_session_li_prefix = Local +ij_java_session_li_suffix = +ij_java_session_ri_prefix = +ij_java_session_ri_suffix = +ij_java_session_si_prefix = +ij_java_session_si_suffix = Service +ij_java_space_after_closing_angle_bracket_in_type_argument = false +ij_java_space_after_colon = true +ij_java_space_after_comma = true +ij_java_space_after_comma_in_type_arguments = true +ij_java_space_after_for_semicolon = true +ij_java_space_after_quest = true +ij_java_space_after_type_cast = true +ij_java_space_before_annotation_array_initializer_left_brace = false +ij_java_space_before_annotation_parameter_list = false +ij_java_space_before_array_initializer_left_brace = false +ij_java_space_before_catch_keyword = true +ij_java_space_before_catch_left_brace = true +ij_java_space_before_catch_parentheses = true +ij_java_space_before_class_left_brace = true +ij_java_space_before_colon = true +ij_java_space_before_colon_in_foreach = true +ij_java_space_before_comma = false +ij_java_space_before_deconstruction_list = false +ij_java_space_before_do_left_brace = true +ij_java_space_before_else_keyword = true +ij_java_space_before_else_left_brace = true +ij_java_space_before_finally_keyword = true +ij_java_space_before_finally_left_brace = true +ij_java_space_before_for_left_brace = true +ij_java_space_before_for_parentheses = true +ij_java_space_before_for_semicolon = false +ij_java_space_before_if_left_brace = true +ij_java_space_before_if_parentheses = true +ij_java_space_before_method_call_parentheses = false +ij_java_space_before_method_left_brace = true +ij_java_space_before_method_parentheses = false +ij_java_space_before_opening_angle_bracket_in_type_parameter = false +ij_java_space_before_quest = true +ij_java_space_before_switch_left_brace = true +ij_java_space_before_switch_parentheses = true +ij_java_space_before_synchronized_left_brace = true +ij_java_space_before_synchronized_parentheses = true +ij_java_space_before_try_left_brace = true +ij_java_space_before_try_parentheses = true +ij_java_space_before_type_parameter_list = false +ij_java_space_before_while_keyword = true +ij_java_space_before_while_left_brace = true +ij_java_space_before_while_parentheses = true +ij_java_space_inside_one_line_enum_braces = false +ij_java_space_within_empty_array_initializer_braces = false +ij_java_space_within_empty_method_call_parentheses = false +ij_java_space_within_empty_method_parentheses = false +ij_java_spaces_around_additive_operators = true +ij_java_spaces_around_annotation_eq = true +ij_java_spaces_around_assignment_operators = true +ij_java_spaces_around_bitwise_operators = true +ij_java_spaces_around_equality_operators = true +ij_java_spaces_around_lambda_arrow = true +ij_java_spaces_around_logical_operators = true +ij_java_spaces_around_method_ref_dbl_colon = false +ij_java_spaces_around_multiplicative_operators = true +ij_java_spaces_around_relational_operators = true +ij_java_spaces_around_shift_operators = true +ij_java_spaces_around_type_bounds_in_type_parameters = true +ij_java_spaces_around_unary_operator = false +ij_java_spaces_inside_block_braces_when_body_is_present = false +ij_java_spaces_within_angle_brackets = false +ij_java_spaces_within_annotation_parentheses = false +ij_java_spaces_within_array_initializer_braces = false +ij_java_spaces_within_braces = false +ij_java_spaces_within_brackets = false +ij_java_spaces_within_cast_parentheses = false +ij_java_spaces_within_catch_parentheses = false +ij_java_spaces_within_deconstruction_list = false +ij_java_spaces_within_for_parentheses = false +ij_java_spaces_within_if_parentheses = false +ij_java_spaces_within_method_call_parentheses = false +ij_java_spaces_within_method_parentheses = false +ij_java_spaces_within_parentheses = false +ij_java_spaces_within_record_header = false +ij_java_spaces_within_switch_parentheses = false +ij_java_spaces_within_synchronized_parentheses = false +ij_java_spaces_within_try_parentheses = false +ij_java_spaces_within_while_parentheses = false +ij_java_special_else_if_treatment = true +ij_java_static_field_name_prefix = +ij_java_static_field_name_suffix = +ij_java_subclass_name_prefix = +ij_java_subclass_name_suffix = Impl +ij_java_switch_expressions_wrap = normal +ij_java_ternary_operation_signs_on_next_line = false +ij_java_ternary_operation_wrap = off +ij_java_test_name_prefix = +ij_java_test_name_suffix = Test +ij_java_throws_keyword_wrap = off +ij_java_throws_list_wrap = off +ij_java_use_external_annotations = false +ij_java_use_fq_class_names = false +ij_java_use_relative_indents = false +ij_java_use_single_class_imports = true +ij_java_variable_annotation_wrap = off +ij_java_visibility = public +ij_java_while_brace_force = never +ij_java_while_on_new_line = false +ij_java_wrap_comments = false +ij_java_wrap_first_method_in_call_chain = false +ij_java_wrap_long_lines = false +ij_java_wrap_semicolon_after_call_chain = false + +[*.less] +indent_size = 2 +ij_less_align_closing_brace_with_properties = false +ij_less_blank_lines_around_nested_selector = 1 +ij_less_blank_lines_between_blocks = 1 +ij_less_block_comment_add_space = false +ij_less_brace_placement = 0 +ij_less_enforce_quotes_on_format = false +ij_less_hex_color_long_format = false +ij_less_hex_color_lower_case = false +ij_less_hex_color_short_format = false +ij_less_hex_color_upper_case = false +ij_less_keep_blank_lines_in_code = 2 +ij_less_keep_indents_on_empty_lines = false +ij_less_keep_single_line_blocks = false +ij_less_line_comment_add_space = false +ij_less_line_comment_at_first_column = false +ij_less_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow +ij_less_space_after_colon = true +ij_less_space_before_opening_brace = true +ij_less_use_double_quotes = true +ij_less_value_alignment = 0 + +[*.proto] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 4 +ij_protobuf_keep_blank_lines_in_code = 2 +ij_protobuf_keep_indents_on_empty_lines = false +ij_protobuf_keep_line_breaks = true +ij_protobuf_space_after_comma = true +ij_protobuf_space_before_comma = false +ij_protobuf_spaces_around_assignment_operators = true +ij_protobuf_spaces_within_braces = false +ij_protobuf_spaces_within_brackets = false + +[*.sass] +indent_size = 2 +ij_sass_align_closing_brace_with_properties = false +ij_sass_blank_lines_around_nested_selector = 1 +ij_sass_blank_lines_between_blocks = 1 +ij_sass_brace_placement = 0 +ij_sass_enforce_quotes_on_format = false +ij_sass_hex_color_long_format = false +ij_sass_hex_color_lower_case = false +ij_sass_hex_color_short_format = false +ij_sass_hex_color_upper_case = false +ij_sass_keep_blank_lines_in_code = 2 +ij_sass_keep_indents_on_empty_lines = false +ij_sass_keep_single_line_blocks = false +ij_sass_line_comment_add_space = false +ij_sass_line_comment_at_first_column = false +ij_sass_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow +ij_sass_space_after_colon = true +ij_sass_space_before_opening_brace = true +ij_sass_use_double_quotes = true +ij_sass_value_alignment = 0 + +[*.scss] +indent_size = 2 +ij_scss_align_closing_brace_with_properties = false +ij_scss_blank_lines_around_nested_selector = 1 +ij_scss_blank_lines_between_blocks = 1 +ij_scss_block_comment_add_space = false +ij_scss_brace_placement = 0 +ij_scss_enforce_quotes_on_format = false +ij_scss_hex_color_long_format = false +ij_scss_hex_color_lower_case = false +ij_scss_hex_color_short_format = false +ij_scss_hex_color_upper_case = false +ij_scss_keep_blank_lines_in_code = 2 +ij_scss_keep_indents_on_empty_lines = false +ij_scss_keep_single_line_blocks = false +ij_scss_line_comment_add_space = false +ij_scss_line_comment_at_first_column = false +ij_scss_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow +ij_scss_space_after_colon = true +ij_scss_space_before_opening_brace = true +ij_scss_use_double_quotes = true +ij_scss_value_alignment = 0 + +[*.vue] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 4 +ij_vue_indent_children_of_top_level = template +ij_vue_interpolation_new_line_after_start_delimiter = true +ij_vue_interpolation_new_line_before_end_delimiter = true +ij_vue_interpolation_wrap = off +ij_vue_keep_indents_on_empty_lines = false +ij_vue_spaces_within_interpolation_expressions = true + +[.editorconfig] +ij_editorconfig_align_group_field_declarations = false +ij_editorconfig_space_after_colon = false +ij_editorconfig_space_after_comma = true +ij_editorconfig_space_before_colon = false +ij_editorconfig_space_before_comma = false +ij_editorconfig_spaces_around_assignment_operators = true + +[{*.ad,*.adoc,*.asciidoc,.asciidoctorconfig}] +ij_asciidoc_blank_lines_after_header = 1 +ij_asciidoc_blank_lines_keep_after_header = 1 +ij_asciidoc_formatting_enabled = true +ij_asciidoc_one_sentence_per_line = true + +[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.pom,*.rng,*.tld,*.wadl,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}] +ij_xml_align_attributes = true +ij_xml_align_text = false +ij_xml_attribute_wrap = normal +ij_xml_block_comment_add_space = false +ij_xml_block_comment_at_first_column = true +ij_xml_keep_blank_lines = 2 +ij_xml_keep_indents_on_empty_lines = false +ij_xml_keep_line_breaks = true +ij_xml_keep_line_breaks_in_text = true +ij_xml_keep_whitespaces = false +ij_xml_keep_whitespaces_around_cdata = preserve +ij_xml_keep_whitespaces_inside_cdata = false +ij_xml_line_comment_at_first_column = true +ij_xml_space_after_tag_name = false +ij_xml_space_around_equals_in_attribute = false +ij_xml_space_inside_empty_tag = false +ij_xml_text_wrap = normal + +[{*.ats,*.cts,*.mts,*.ts}] +ij_continuation_indent_size = 4 +ij_typescript_align_imports = false +ij_typescript_align_multiline_array_initializer_expression = false +ij_typescript_align_multiline_binary_operation = false +ij_typescript_align_multiline_chained_methods = false +ij_typescript_align_multiline_extends_list = false +ij_typescript_align_multiline_for = true +ij_typescript_align_multiline_parameters = true +ij_typescript_align_multiline_parameters_in_calls = false +ij_typescript_align_multiline_ternary_operation = false +ij_typescript_align_object_properties = 0 +ij_typescript_align_union_types = false +ij_typescript_align_var_statements = 0 +ij_typescript_array_initializer_new_line_after_left_brace = false +ij_typescript_array_initializer_right_brace_on_new_line = false +ij_typescript_array_initializer_wrap = off +ij_typescript_assignment_wrap = off +ij_typescript_binary_operation_sign_on_next_line = false +ij_typescript_binary_operation_wrap = off +ij_typescript_blacklist_imports = rxjs/Rx, node_modules/**, **/node_modules/**, @angular/material, @angular/material/typings/** +ij_typescript_blank_lines_after_imports = 1 +ij_typescript_blank_lines_around_class = 1 +ij_typescript_blank_lines_around_field = 0 +ij_typescript_blank_lines_around_field_in_interface = 0 +ij_typescript_blank_lines_around_function = 1 +ij_typescript_blank_lines_around_method = 1 +ij_typescript_blank_lines_around_method_in_interface = 1 +ij_typescript_block_brace_style = end_of_line +ij_typescript_block_comment_add_space = false +ij_typescript_block_comment_at_first_column = true +ij_typescript_call_parameters_new_line_after_left_paren = false +ij_typescript_call_parameters_right_paren_on_new_line = false +ij_typescript_call_parameters_wrap = off +ij_typescript_catch_on_new_line = false +ij_typescript_chained_call_dot_on_new_line = true +ij_typescript_class_brace_style = end_of_line +ij_typescript_class_decorator_wrap = split_into_lines +ij_typescript_class_field_decorator_wrap = off +ij_typescript_class_method_decorator_wrap = off +ij_typescript_comma_on_new_line = false +ij_typescript_do_while_brace_force = never +ij_typescript_else_on_new_line = false +ij_typescript_enforce_trailing_comma = keep +ij_typescript_enum_constants_wrap = on_every_item +ij_typescript_extends_keyword_wrap = off +ij_typescript_extends_list_wrap = off +ij_typescript_field_prefix = _ +ij_typescript_file_name_style = relaxed +ij_typescript_finally_on_new_line = false +ij_typescript_for_brace_force = never +ij_typescript_for_statement_new_line_after_left_paren = false +ij_typescript_for_statement_right_paren_on_new_line = false +ij_typescript_for_statement_wrap = off +ij_typescript_force_quote_style = false +ij_typescript_force_semicolon_style = false +ij_typescript_function_expression_brace_style = end_of_line +ij_typescript_function_parameter_decorator_wrap = off +ij_typescript_if_brace_force = never +ij_typescript_import_merge_members = global +ij_typescript_import_prefer_absolute_path = global +ij_typescript_import_sort_members = true +ij_typescript_import_sort_module_name = false +ij_typescript_import_use_node_resolution = true +ij_typescript_imports_wrap = on_every_item +ij_typescript_indent_case_from_switch = true +ij_typescript_indent_chained_calls = true +ij_typescript_indent_package_children = 0 +ij_typescript_jsdoc_include_types = false +ij_typescript_jsx_attribute_value = braces +ij_typescript_keep_blank_lines_in_code = 2 +ij_typescript_keep_first_column_comment = true +ij_typescript_keep_indents_on_empty_lines = false +ij_typescript_keep_line_breaks = true +ij_typescript_keep_simple_blocks_in_one_line = false +ij_typescript_keep_simple_methods_in_one_line = false +ij_typescript_line_comment_add_space = true +ij_typescript_line_comment_at_first_column = false +ij_typescript_method_brace_style = end_of_line +ij_typescript_method_call_chain_wrap = off +ij_typescript_method_parameters_new_line_after_left_paren = false +ij_typescript_method_parameters_right_paren_on_new_line = false +ij_typescript_method_parameters_wrap = off +ij_typescript_object_literal_wrap = on_every_item +ij_typescript_object_types_wrap = on_every_item +ij_typescript_parentheses_expression_new_line_after_left_paren = false +ij_typescript_parentheses_expression_right_paren_on_new_line = false +ij_typescript_place_assignment_sign_on_next_line = false +ij_typescript_prefer_as_type_cast = false +ij_typescript_prefer_explicit_types_function_expression_returns = false +ij_typescript_prefer_explicit_types_function_returns = false +ij_typescript_prefer_explicit_types_vars_fields = false +ij_typescript_prefer_parameters_wrap = false +ij_typescript_property_prefix = +ij_typescript_reformat_c_style_comments = false +ij_typescript_space_after_colon = true +ij_typescript_space_after_comma = true +ij_typescript_space_after_dots_in_rest_parameter = false +ij_typescript_space_after_generator_mult = true +ij_typescript_space_after_property_colon = true +ij_typescript_space_after_quest = true +ij_typescript_space_after_type_colon = true +ij_typescript_space_after_unary_not = false +ij_typescript_space_before_async_arrow_lparen = true +ij_typescript_space_before_catch_keyword = true +ij_typescript_space_before_catch_left_brace = true +ij_typescript_space_before_catch_parentheses = true +ij_typescript_space_before_class_lbrace = true +ij_typescript_space_before_class_left_brace = true +ij_typescript_space_before_colon = true +ij_typescript_space_before_comma = false +ij_typescript_space_before_do_left_brace = true +ij_typescript_space_before_else_keyword = true +ij_typescript_space_before_else_left_brace = true +ij_typescript_space_before_finally_keyword = true +ij_typescript_space_before_finally_left_brace = true +ij_typescript_space_before_for_left_brace = true +ij_typescript_space_before_for_parentheses = true +ij_typescript_space_before_for_semicolon = false +ij_typescript_space_before_function_left_parenth = true +ij_typescript_space_before_generator_mult = false +ij_typescript_space_before_if_left_brace = true +ij_typescript_space_before_if_parentheses = true +ij_typescript_space_before_method_call_parentheses = false +ij_typescript_space_before_method_left_brace = true +ij_typescript_space_before_method_parentheses = false +ij_typescript_space_before_property_colon = false +ij_typescript_space_before_quest = true +ij_typescript_space_before_switch_left_brace = true +ij_typescript_space_before_switch_parentheses = true +ij_typescript_space_before_try_left_brace = true +ij_typescript_space_before_type_colon = false +ij_typescript_space_before_unary_not = false +ij_typescript_space_before_while_keyword = true +ij_typescript_space_before_while_left_brace = true +ij_typescript_space_before_while_parentheses = true +ij_typescript_spaces_around_additive_operators = true +ij_typescript_spaces_around_arrow_function_operator = true +ij_typescript_spaces_around_assignment_operators = true +ij_typescript_spaces_around_bitwise_operators = true +ij_typescript_spaces_around_equality_operators = true +ij_typescript_spaces_around_logical_operators = true +ij_typescript_spaces_around_multiplicative_operators = true +ij_typescript_spaces_around_relational_operators = true +ij_typescript_spaces_around_shift_operators = true +ij_typescript_spaces_around_unary_operator = false +ij_typescript_spaces_within_array_initializer_brackets = false +ij_typescript_spaces_within_brackets = false +ij_typescript_spaces_within_catch_parentheses = false +ij_typescript_spaces_within_for_parentheses = false +ij_typescript_spaces_within_if_parentheses = false +ij_typescript_spaces_within_imports = false +ij_typescript_spaces_within_interpolation_expressions = false +ij_typescript_spaces_within_method_call_parentheses = false +ij_typescript_spaces_within_method_parentheses = false +ij_typescript_spaces_within_object_literal_braces = false +ij_typescript_spaces_within_object_type_braces = true +ij_typescript_spaces_within_parentheses = false +ij_typescript_spaces_within_switch_parentheses = false +ij_typescript_spaces_within_type_assertion = false +ij_typescript_spaces_within_union_types = true +ij_typescript_spaces_within_while_parentheses = false +ij_typescript_special_else_if_treatment = true +ij_typescript_ternary_operation_signs_on_next_line = false +ij_typescript_ternary_operation_wrap = off +ij_typescript_union_types_wrap = on_every_item +ij_typescript_use_chained_calls_group_indents = false +ij_typescript_use_double_quotes = true +ij_typescript_use_explicit_js_extension = auto +ij_typescript_use_import_type = auto +ij_typescript_use_path_mapping = always +ij_typescript_use_public_modifier = false +ij_typescript_use_semicolon_after_statement = true +ij_typescript_var_declaration_wrap = normal +ij_typescript_while_brace_force = never +ij_typescript_while_on_new_line = false +ij_typescript_wrap_comments = false + +[{*.bash,*.sh,*.zsh}] +indent_size = 2 +tab_width = 2 +ij_shell_binary_ops_start_line = false +ij_shell_keep_column_alignment_padding = false +ij_shell_minify_program = false +ij_shell_redirect_followed_by_space = false +ij_shell_switch_cases_indented = false +ij_shell_use_unix_line_separator = true + +[{*.cjs,*.es6,*.js,*.mjs}] +ij_continuation_indent_size = 4 +ij_javascript_align_imports = false +ij_javascript_align_multiline_array_initializer_expression = false +ij_javascript_align_multiline_binary_operation = false +ij_javascript_align_multiline_chained_methods = false +ij_javascript_align_multiline_extends_list = false +ij_javascript_align_multiline_for = true +ij_javascript_align_multiline_parameters = true +ij_javascript_align_multiline_parameters_in_calls = false +ij_javascript_align_multiline_ternary_operation = false +ij_javascript_align_object_properties = 0 +ij_javascript_align_union_types = false +ij_javascript_align_var_statements = 0 +ij_javascript_array_initializer_new_line_after_left_brace = false +ij_javascript_array_initializer_right_brace_on_new_line = false +ij_javascript_array_initializer_wrap = off +ij_javascript_assignment_wrap = off +ij_javascript_binary_operation_sign_on_next_line = false +ij_javascript_binary_operation_wrap = off +ij_javascript_blacklist_imports = rxjs/Rx, node_modules/**, **/node_modules/**, @angular/material, @angular/material/typings/** +ij_javascript_blank_lines_after_imports = 1 +ij_javascript_blank_lines_around_class = 1 +ij_javascript_blank_lines_around_field = 0 +ij_javascript_blank_lines_around_function = 1 +ij_javascript_blank_lines_around_method = 1 +ij_javascript_block_brace_style = end_of_line +ij_javascript_block_comment_add_space = false +ij_javascript_block_comment_at_first_column = true +ij_javascript_call_parameters_new_line_after_left_paren = false +ij_javascript_call_parameters_right_paren_on_new_line = false +ij_javascript_call_parameters_wrap = off +ij_javascript_catch_on_new_line = false +ij_javascript_chained_call_dot_on_new_line = true +ij_javascript_class_brace_style = end_of_line +ij_javascript_class_decorator_wrap = split_into_lines +ij_javascript_class_field_decorator_wrap = off +ij_javascript_class_method_decorator_wrap = off +ij_javascript_comma_on_new_line = false +ij_javascript_do_while_brace_force = never +ij_javascript_else_on_new_line = false +ij_javascript_enforce_trailing_comma = keep +ij_javascript_extends_keyword_wrap = off +ij_javascript_extends_list_wrap = off +ij_javascript_field_prefix = _ +ij_javascript_file_name_style = relaxed +ij_javascript_finally_on_new_line = false +ij_javascript_for_brace_force = never +ij_javascript_for_statement_new_line_after_left_paren = false +ij_javascript_for_statement_right_paren_on_new_line = false +ij_javascript_for_statement_wrap = off +ij_javascript_force_quote_style = false +ij_javascript_force_semicolon_style = false +ij_javascript_function_expression_brace_style = end_of_line +ij_javascript_function_parameter_decorator_wrap = off +ij_javascript_if_brace_force = never +ij_javascript_import_merge_members = global +ij_javascript_import_prefer_absolute_path = global +ij_javascript_import_sort_members = true +ij_javascript_import_sort_module_name = false +ij_javascript_import_use_node_resolution = true +ij_javascript_imports_wrap = on_every_item +ij_javascript_indent_case_from_switch = true +ij_javascript_indent_chained_calls = true +ij_javascript_indent_package_children = 0 +ij_javascript_jsx_attribute_value = braces +ij_javascript_keep_blank_lines_in_code = 2 +ij_javascript_keep_first_column_comment = true +ij_javascript_keep_indents_on_empty_lines = false +ij_javascript_keep_line_breaks = true +ij_javascript_keep_simple_blocks_in_one_line = false +ij_javascript_keep_simple_methods_in_one_line = false +ij_javascript_line_comment_add_space = true +ij_javascript_line_comment_at_first_column = false +ij_javascript_method_brace_style = end_of_line +ij_javascript_method_call_chain_wrap = off +ij_javascript_method_parameters_new_line_after_left_paren = false +ij_javascript_method_parameters_right_paren_on_new_line = false +ij_javascript_method_parameters_wrap = off +ij_javascript_object_literal_wrap = on_every_item +ij_javascript_object_types_wrap = on_every_item +ij_javascript_parentheses_expression_new_line_after_left_paren = false +ij_javascript_parentheses_expression_right_paren_on_new_line = false +ij_javascript_place_assignment_sign_on_next_line = false +ij_javascript_prefer_as_type_cast = false +ij_javascript_prefer_explicit_types_function_expression_returns = false +ij_javascript_prefer_explicit_types_function_returns = false +ij_javascript_prefer_explicit_types_vars_fields = false +ij_javascript_prefer_parameters_wrap = false +ij_javascript_property_prefix = +ij_javascript_reformat_c_style_comments = false +ij_javascript_space_after_colon = true +ij_javascript_space_after_comma = true +ij_javascript_space_after_dots_in_rest_parameter = false +ij_javascript_space_after_generator_mult = true +ij_javascript_space_after_property_colon = true +ij_javascript_space_after_quest = true +ij_javascript_space_after_type_colon = true +ij_javascript_space_after_unary_not = false +ij_javascript_space_before_async_arrow_lparen = true +ij_javascript_space_before_catch_keyword = true +ij_javascript_space_before_catch_left_brace = true +ij_javascript_space_before_catch_parentheses = true +ij_javascript_space_before_class_lbrace = true +ij_javascript_space_before_class_left_brace = true +ij_javascript_space_before_colon = true +ij_javascript_space_before_comma = false +ij_javascript_space_before_do_left_brace = true +ij_javascript_space_before_else_keyword = true +ij_javascript_space_before_else_left_brace = true +ij_javascript_space_before_finally_keyword = true +ij_javascript_space_before_finally_left_brace = true +ij_javascript_space_before_for_left_brace = true +ij_javascript_space_before_for_parentheses = true +ij_javascript_space_before_for_semicolon = false +ij_javascript_space_before_function_left_parenth = true +ij_javascript_space_before_generator_mult = false +ij_javascript_space_before_if_left_brace = true +ij_javascript_space_before_if_parentheses = true +ij_javascript_space_before_method_call_parentheses = false +ij_javascript_space_before_method_left_brace = true +ij_javascript_space_before_method_parentheses = false +ij_javascript_space_before_property_colon = false +ij_javascript_space_before_quest = true +ij_javascript_space_before_switch_left_brace = true +ij_javascript_space_before_switch_parentheses = true +ij_javascript_space_before_try_left_brace = true +ij_javascript_space_before_type_colon = false +ij_javascript_space_before_unary_not = false +ij_javascript_space_before_while_keyword = true +ij_javascript_space_before_while_left_brace = true +ij_javascript_space_before_while_parentheses = true +ij_javascript_spaces_around_additive_operators = true +ij_javascript_spaces_around_arrow_function_operator = true +ij_javascript_spaces_around_assignment_operators = true +ij_javascript_spaces_around_bitwise_operators = true +ij_javascript_spaces_around_equality_operators = true +ij_javascript_spaces_around_logical_operators = true +ij_javascript_spaces_around_multiplicative_operators = true +ij_javascript_spaces_around_relational_operators = true +ij_javascript_spaces_around_shift_operators = true +ij_javascript_spaces_around_unary_operator = false +ij_javascript_spaces_within_array_initializer_brackets = false +ij_javascript_spaces_within_brackets = false +ij_javascript_spaces_within_catch_parentheses = false +ij_javascript_spaces_within_for_parentheses = false +ij_javascript_spaces_within_if_parentheses = false +ij_javascript_spaces_within_imports = false +ij_javascript_spaces_within_interpolation_expressions = false +ij_javascript_spaces_within_method_call_parentheses = false +ij_javascript_spaces_within_method_parentheses = false +ij_javascript_spaces_within_object_literal_braces = false +ij_javascript_spaces_within_object_type_braces = true +ij_javascript_spaces_within_parentheses = false +ij_javascript_spaces_within_switch_parentheses = false +ij_javascript_spaces_within_type_assertion = false +ij_javascript_spaces_within_union_types = true +ij_javascript_spaces_within_while_parentheses = false +ij_javascript_special_else_if_treatment = true +ij_javascript_ternary_operation_signs_on_next_line = false +ij_javascript_ternary_operation_wrap = off +ij_javascript_union_types_wrap = on_every_item +ij_javascript_use_chained_calls_group_indents = false +ij_javascript_use_double_quotes = true +ij_javascript_use_explicit_js_extension = auto +ij_javascript_use_import_type = auto +ij_javascript_use_path_mapping = always +ij_javascript_use_public_modifier = false +ij_javascript_use_semicolon_after_statement = true +ij_javascript_var_declaration_wrap = normal +ij_javascript_while_brace_force = never +ij_javascript_while_on_new_line = false +ij_javascript_wrap_comments = false + +[{*.cjsx,*.coffee}] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 2 +ij_coffeescript_align_function_body = false +ij_coffeescript_align_imports = false +ij_coffeescript_align_multiline_array_initializer_expression = true +ij_coffeescript_align_multiline_parameters = true +ij_coffeescript_align_multiline_parameters_in_calls = false +ij_coffeescript_align_object_properties = 0 +ij_coffeescript_align_union_types = false +ij_coffeescript_align_var_statements = 0 +ij_coffeescript_array_initializer_new_line_after_left_brace = false +ij_coffeescript_array_initializer_right_brace_on_new_line = false +ij_coffeescript_array_initializer_wrap = normal +ij_coffeescript_blacklist_imports = rxjs/Rx, node_modules/**, **/node_modules/**, @angular/material, @angular/material/typings/** +ij_coffeescript_blank_lines_around_function = 1 +ij_coffeescript_call_parameters_new_line_after_left_paren = false +ij_coffeescript_call_parameters_right_paren_on_new_line = false +ij_coffeescript_call_parameters_wrap = normal +ij_coffeescript_chained_call_dot_on_new_line = true +ij_coffeescript_class_decorator_wrap = split_into_lines +ij_coffeescript_class_field_decorator_wrap = off +ij_coffeescript_class_method_decorator_wrap = off +ij_coffeescript_comma_on_new_line = false +ij_coffeescript_enforce_trailing_comma = keep +ij_coffeescript_field_prefix = _ +ij_coffeescript_file_name_style = relaxed +ij_coffeescript_force_quote_style = false +ij_coffeescript_force_semicolon_style = false +ij_coffeescript_function_expression_brace_style = end_of_line +ij_coffeescript_function_parameter_decorator_wrap = off +ij_coffeescript_import_merge_members = global +ij_coffeescript_import_prefer_absolute_path = global +ij_coffeescript_import_sort_members = true +ij_coffeescript_import_sort_module_name = false +ij_coffeescript_import_use_node_resolution = true +ij_coffeescript_imports_wrap = on_every_item +ij_coffeescript_indent_chained_calls = true +ij_coffeescript_indent_package_children = 0 +ij_coffeescript_jsx_attribute_value = braces +ij_coffeescript_keep_blank_lines_in_code = 2 +ij_coffeescript_keep_first_column_comment = true +ij_coffeescript_keep_indents_on_empty_lines = false +ij_coffeescript_keep_line_breaks = true +ij_coffeescript_keep_simple_methods_in_one_line = false +ij_coffeescript_method_parameters_new_line_after_left_paren = false +ij_coffeescript_method_parameters_right_paren_on_new_line = false +ij_coffeescript_method_parameters_wrap = off +ij_coffeescript_object_literal_wrap = on_every_item +ij_coffeescript_object_types_wrap = on_every_item +ij_coffeescript_prefer_as_type_cast = false +ij_coffeescript_prefer_explicit_types_function_expression_returns = false +ij_coffeescript_prefer_explicit_types_function_returns = false +ij_coffeescript_prefer_explicit_types_vars_fields = false +ij_coffeescript_property_prefix = +ij_coffeescript_reformat_c_style_comments = false +ij_coffeescript_space_after_comma = true +ij_coffeescript_space_after_dots_in_rest_parameter = false +ij_coffeescript_space_after_generator_mult = true +ij_coffeescript_space_after_property_colon = true +ij_coffeescript_space_after_type_colon = true +ij_coffeescript_space_after_unary_not = false +ij_coffeescript_space_before_async_arrow_lparen = true +ij_coffeescript_space_before_class_lbrace = true +ij_coffeescript_space_before_comma = false +ij_coffeescript_space_before_function_left_parenth = true +ij_coffeescript_space_before_generator_mult = false +ij_coffeescript_space_before_property_colon = false +ij_coffeescript_space_before_type_colon = false +ij_coffeescript_space_before_unary_not = false +ij_coffeescript_spaces_around_additive_operators = true +ij_coffeescript_spaces_around_arrow_function_operator = true +ij_coffeescript_spaces_around_assignment_operators = true +ij_coffeescript_spaces_around_bitwise_operators = true +ij_coffeescript_spaces_around_equality_operators = true +ij_coffeescript_spaces_around_logical_operators = true +ij_coffeescript_spaces_around_multiplicative_operators = true +ij_coffeescript_spaces_around_relational_operators = true +ij_coffeescript_spaces_around_shift_operators = true +ij_coffeescript_spaces_around_unary_operator = false +ij_coffeescript_spaces_within_array_initializer_braces = false +ij_coffeescript_spaces_within_array_initializer_brackets = false +ij_coffeescript_spaces_within_imports = false +ij_coffeescript_spaces_within_index_brackets = false +ij_coffeescript_spaces_within_interpolation_expressions = false +ij_coffeescript_spaces_within_method_call_parentheses = false +ij_coffeescript_spaces_within_method_parentheses = false +ij_coffeescript_spaces_within_object_braces = false +ij_coffeescript_spaces_within_object_literal_braces = false +ij_coffeescript_spaces_within_object_type_braces = true +ij_coffeescript_spaces_within_range_brackets = false +ij_coffeescript_spaces_within_type_assertion = false +ij_coffeescript_spaces_within_union_types = true +ij_coffeescript_union_types_wrap = on_every_item +ij_coffeescript_use_chained_calls_group_indents = false +ij_coffeescript_use_double_quotes = true +ij_coffeescript_use_explicit_js_extension = auto +ij_coffeescript_use_import_type = auto +ij_coffeescript_use_path_mapping = always +ij_coffeescript_use_public_modifier = false +ij_coffeescript_use_semicolon_after_statement = false +ij_coffeescript_var_declaration_wrap = normal + +[{*.ft,*.vm,*.vsl}] +ij_vtl_keep_indents_on_empty_lines = false + +[{*.gant,*.groovy,*.gson,*.gy}] +ij_groovy_align_group_field_declarations = false +ij_groovy_align_multiline_array_initializer_expression = false +ij_groovy_align_multiline_assignment = false +ij_groovy_align_multiline_binary_operation = false +ij_groovy_align_multiline_chained_methods = false +ij_groovy_align_multiline_extends_list = false +ij_groovy_align_multiline_for = true +ij_groovy_align_multiline_list_or_map = true +ij_groovy_align_multiline_method_parentheses = false +ij_groovy_align_multiline_parameters = true +ij_groovy_align_multiline_parameters_in_calls = false +ij_groovy_align_multiline_resources = true +ij_groovy_align_multiline_ternary_operation = false +ij_groovy_align_multiline_throws_list = false +ij_groovy_align_named_args_in_map = true +ij_groovy_align_throws_keyword = false +ij_groovy_array_initializer_new_line_after_left_brace = false +ij_groovy_array_initializer_right_brace_on_new_line = false +ij_groovy_array_initializer_wrap = off +ij_groovy_assert_statement_wrap = off +ij_groovy_assignment_wrap = off +ij_groovy_binary_operation_wrap = off +ij_groovy_blank_lines_after_class_header = 0 +ij_groovy_blank_lines_after_imports = 1 +ij_groovy_blank_lines_after_package = 1 +ij_groovy_blank_lines_around_class = 1 +ij_groovy_blank_lines_around_field = 0 +ij_groovy_blank_lines_around_field_in_interface = 0 +ij_groovy_blank_lines_around_method = 1 +ij_groovy_blank_lines_around_method_in_interface = 1 +ij_groovy_blank_lines_before_imports = 1 +ij_groovy_blank_lines_before_method_body = 0 +ij_groovy_blank_lines_before_package = 0 +ij_groovy_block_brace_style = end_of_line +ij_groovy_block_comment_add_space = false +ij_groovy_block_comment_at_first_column = true +ij_groovy_call_parameters_new_line_after_left_paren = false +ij_groovy_call_parameters_right_paren_on_new_line = false +ij_groovy_call_parameters_wrap = off +ij_groovy_catch_on_new_line = false +ij_groovy_class_annotation_wrap = split_into_lines +ij_groovy_class_brace_style = end_of_line +ij_groovy_class_count_to_use_import_on_demand = 999 +ij_groovy_do_while_brace_force = never +ij_groovy_else_on_new_line = false +ij_groovy_enable_groovydoc_formatting = true +ij_groovy_enum_constants_wrap = off +ij_groovy_extends_keyword_wrap = off +ij_groovy_extends_list_wrap = off +ij_groovy_field_annotation_wrap = split_into_lines +ij_groovy_finally_on_new_line = false +ij_groovy_for_brace_force = never +ij_groovy_for_statement_new_line_after_left_paren = false +ij_groovy_for_statement_right_paren_on_new_line = false +ij_groovy_for_statement_wrap = off +ij_groovy_ginq_general_clause_wrap_policy = 2 +ij_groovy_ginq_having_wrap_policy = 1 +ij_groovy_ginq_indent_having_clause = true +ij_groovy_ginq_indent_on_clause = true +ij_groovy_ginq_on_wrap_policy = 1 +ij_groovy_ginq_space_after_keyword = true +ij_groovy_if_brace_force = never +ij_groovy_import_annotation_wrap = 2 +ij_groovy_imports_layout = java.**, |, javax.**, |, groovy.**, org.apache.groovy.**, org.codehaus.groovy.**, |, jakarta.**, |, *, |, io.spring.**, org.springframework.**, |, grails.**, org.apache.grails.**, org.grails.**, |, $* +ij_groovy_indent_case_from_switch = true +ij_groovy_indent_label_blocks = true +ij_groovy_insert_inner_class_imports = false +ij_groovy_keep_blank_lines_before_right_brace = 2 +ij_groovy_keep_blank_lines_in_code = 2 +ij_groovy_keep_blank_lines_in_declarations = 2 +ij_groovy_keep_control_statement_in_one_line = true +ij_groovy_keep_first_column_comment = true +ij_groovy_keep_indents_on_empty_lines = false +ij_groovy_keep_line_breaks = true +ij_groovy_keep_multiple_expressions_in_one_line = false +ij_groovy_keep_simple_blocks_in_one_line = false +ij_groovy_keep_simple_classes_in_one_line = true +ij_groovy_keep_simple_lambdas_in_one_line = true +ij_groovy_keep_simple_methods_in_one_line = true +ij_groovy_label_indent_absolute = false +ij_groovy_label_indent_size = 0 +ij_groovy_lambda_brace_style = end_of_line +ij_groovy_layout_static_imports_separately = true +ij_groovy_line_comment_add_space = false +ij_groovy_line_comment_add_space_on_reformat = false +ij_groovy_line_comment_at_first_column = true +ij_groovy_method_annotation_wrap = split_into_lines +ij_groovy_method_brace_style = end_of_line +ij_groovy_method_call_chain_wrap = off +ij_groovy_method_parameters_new_line_after_left_paren = false +ij_groovy_method_parameters_right_paren_on_new_line = false +ij_groovy_method_parameters_wrap = off +ij_groovy_modifier_list_wrap = false +ij_groovy_names_count_to_use_import_on_demand = 999 +ij_groovy_packages_to_use_import_on_demand = +ij_groovy_parameter_annotation_wrap = off +ij_groovy_parentheses_expression_new_line_after_left_paren = false +ij_groovy_parentheses_expression_right_paren_on_new_line = false +ij_groovy_prefer_parameters_wrap = false +ij_groovy_resource_list_new_line_after_left_paren = false +ij_groovy_resource_list_right_paren_on_new_line = false +ij_groovy_resource_list_wrap = off +ij_groovy_space_after_assert_separator = true +ij_groovy_space_after_colon = true +ij_groovy_space_after_comma = true +ij_groovy_space_after_comma_in_type_arguments = true +ij_groovy_space_after_for_semicolon = true +ij_groovy_space_after_quest = true +ij_groovy_space_after_type_cast = true +ij_groovy_space_before_annotation_parameter_list = false +ij_groovy_space_before_array_initializer_left_brace = false +ij_groovy_space_before_assert_separator = false +ij_groovy_space_before_catch_keyword = true +ij_groovy_space_before_catch_left_brace = true +ij_groovy_space_before_catch_parentheses = true +ij_groovy_space_before_class_left_brace = true +ij_groovy_space_before_closure_left_brace = true +ij_groovy_space_before_colon = true +ij_groovy_space_before_comma = false +ij_groovy_space_before_do_left_brace = true +ij_groovy_space_before_else_keyword = true +ij_groovy_space_before_else_left_brace = true +ij_groovy_space_before_finally_keyword = true +ij_groovy_space_before_finally_left_brace = true +ij_groovy_space_before_for_left_brace = true +ij_groovy_space_before_for_parentheses = true +ij_groovy_space_before_for_semicolon = false +ij_groovy_space_before_if_left_brace = true +ij_groovy_space_before_if_parentheses = true +ij_groovy_space_before_method_call_parentheses = false +ij_groovy_space_before_method_left_brace = true +ij_groovy_space_before_method_parentheses = false +ij_groovy_space_before_quest = true +ij_groovy_space_before_record_parentheses = false +ij_groovy_space_before_switch_left_brace = true +ij_groovy_space_before_switch_parentheses = true +ij_groovy_space_before_synchronized_left_brace = true +ij_groovy_space_before_synchronized_parentheses = true +ij_groovy_space_before_try_left_brace = true +ij_groovy_space_before_try_parentheses = true +ij_groovy_space_before_while_keyword = true +ij_groovy_space_before_while_left_brace = true +ij_groovy_space_before_while_parentheses = true +ij_groovy_space_in_named_argument = true +ij_groovy_space_in_named_argument_before_colon = false +ij_groovy_space_within_empty_array_initializer_braces = false +ij_groovy_space_within_empty_method_call_parentheses = false +ij_groovy_spaces_around_additive_operators = true +ij_groovy_spaces_around_assignment_operators = true +ij_groovy_spaces_around_bitwise_operators = true +ij_groovy_spaces_around_equality_operators = true +ij_groovy_spaces_around_lambda_arrow = true +ij_groovy_spaces_around_logical_operators = true +ij_groovy_spaces_around_multiplicative_operators = true +ij_groovy_spaces_around_regex_operators = true +ij_groovy_spaces_around_relational_operators = true +ij_groovy_spaces_around_shift_operators = true +ij_groovy_spaces_within_annotation_parentheses = false +ij_groovy_spaces_within_array_initializer_braces = false +ij_groovy_spaces_within_braces = true +ij_groovy_spaces_within_brackets = false +ij_groovy_spaces_within_cast_parentheses = false +ij_groovy_spaces_within_catch_parentheses = false +ij_groovy_spaces_within_for_parentheses = false +ij_groovy_spaces_within_gstring_injection_braces = false +ij_groovy_spaces_within_if_parentheses = false +ij_groovy_spaces_within_list_or_map = false +ij_groovy_spaces_within_method_call_parentheses = false +ij_groovy_spaces_within_method_parentheses = false +ij_groovy_spaces_within_parentheses = false +ij_groovy_spaces_within_switch_parentheses = false +ij_groovy_spaces_within_synchronized_parentheses = false +ij_groovy_spaces_within_try_parentheses = false +ij_groovy_spaces_within_tuple_expression = false +ij_groovy_spaces_within_while_parentheses = false +ij_groovy_special_else_if_treatment = true +ij_groovy_ternary_operation_wrap = off +ij_groovy_throws_keyword_wrap = off +ij_groovy_throws_list_wrap = off +ij_groovy_use_flying_geese_braces = false +ij_groovy_use_fq_class_names = false +ij_groovy_use_fq_class_names_in_javadoc = true +ij_groovy_use_relative_indents = false +ij_groovy_use_single_class_imports = true +ij_groovy_variable_annotation_wrap = off +ij_groovy_while_brace_force = never +ij_groovy_while_on_new_line = false +ij_groovy_wrap_chain_calls_after_dot = false +ij_groovy_wrap_long_lines = false + +[{*.har,*.jsb2,*.jsb3,*.json,*.jsonc,*.postman_collection,*.postman_collection.json,*.postman_environment,*.postman_environment.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,.ws-context,jest.config}] +indent_size = 2 +ij_json_array_wrapping = split_into_lines +ij_json_keep_blank_lines_in_code = 0 +ij_json_keep_indents_on_empty_lines = false +ij_json_keep_line_breaks = true +ij_json_keep_trailing_comma = false +ij_json_object_wrapping = split_into_lines +ij_json_property_alignment = do_not_align +ij_json_space_after_colon = true +ij_json_space_after_comma = true +ij_json_space_before_colon = false +ij_json_space_before_comma = false +ij_json_spaces_within_braces = false +ij_json_spaces_within_brackets = false +ij_json_wrap_long_lines = false + +[{*.hcl,*.nomad}] +indent_size = 2 +ij_hcl_keep_blank_lines_in_code = 2 +ij_hcl_keep_indents_on_empty_lines = false +ij_hcl_keep_line_breaks = true +ij_hcl_space_after_comma = true +ij_hcl_space_before_comma = false +ij_hcl_spaces_around_assignment_operators = true +ij_hcl_spaces_within_braces = false +ij_hcl_spaces_within_brackets = false +ij_hcl_wrap_long_lines = false + +[{*.htm,*.html,*.sht,*.shtm,*.shtml}] +ij_html_add_new_line_before_tags = body, div, p, form, h1, h2, h3 +ij_html_align_attributes = true +ij_html_align_text = false +ij_html_attribute_wrap = normal +ij_html_block_comment_add_space = false +ij_html_block_comment_at_first_column = true +ij_html_do_not_align_children_of_min_lines = 0 +ij_html_do_not_break_if_inline_tags = title, h1, h2, h3, h4, h5, h6, p +ij_html_do_not_indent_children_of_tags = html, body, thead, tbody, tfoot +ij_html_enforce_quotes = false +ij_html_inline_tags = a, abbr, acronym, b, basefont, bdo, big, br, cite, cite, code, dfn, em, font, i, img, input, kbd, label, q, s, samp, select, small, span, strike, strong, sub, sup, textarea, tt, u, var +ij_html_keep_blank_lines = 2 +ij_html_keep_indents_on_empty_lines = false +ij_html_keep_line_breaks = true +ij_html_keep_line_breaks_in_text = true +ij_html_keep_whitespaces = false +ij_html_keep_whitespaces_inside = span, pre, textarea +ij_html_line_comment_at_first_column = true +ij_html_new_line_after_last_attribute = never +ij_html_new_line_before_first_attribute = never +ij_html_quote_style = double +ij_html_remove_new_line_before_tags = br +ij_html_space_after_tag_name = false +ij_html_space_around_equality_in_attribute = false +ij_html_space_inside_empty_tag = false +ij_html_text_wrap = normal + +[{*.http,*.rest}] +ij_continuation_indent_size = 4 +ij_http-request_call_parameters_wrap = normal +ij_http-request_method_parameters_wrap = split_into_lines +ij_http-request_space_before_comma = true +ij_http-request_spaces_around_assignment_operators = true + +[{*.jsf,*.jsp,*.jspf,*.tag,*.tagf,*.xjsp}] +ij_jsp_jsp_prefer_comma_separated_import_list = false +ij_jsp_keep_indents_on_empty_lines = false + +[{*.jspx,*.tagx}] +ij_jspx_keep_indents_on_empty_lines = false + +[{*.kt,*.kts}] +ij_kotlin_align_in_columns_case_branch = false +ij_kotlin_align_multiline_binary_operation = false +ij_kotlin_align_multiline_extends_list = false +ij_kotlin_align_multiline_method_parentheses = false +ij_kotlin_align_multiline_parameters = true +ij_kotlin_align_multiline_parameters_in_calls = false +ij_kotlin_allow_trailing_comma = false +ij_kotlin_allow_trailing_comma_collection_literal_expression = false +ij_kotlin_allow_trailing_comma_context_receiver_list = true +ij_kotlin_allow_trailing_comma_destructuring_declaration = true +ij_kotlin_allow_trailing_comma_function_literal = true +ij_kotlin_allow_trailing_comma_indices = false +ij_kotlin_allow_trailing_comma_on_call_site = false +ij_kotlin_allow_trailing_comma_type_argument_list = false +ij_kotlin_allow_trailing_comma_type_parameter_list = true +ij_kotlin_allow_trailing_comma_value_argument_list = false +ij_kotlin_allow_trailing_comma_value_parameter_list = true +ij_kotlin_allow_trailing_comma_when_entry = true +ij_kotlin_assignment_wrap = normal +ij_kotlin_blank_lines_after_class_header = 0 +ij_kotlin_blank_lines_around_block_when_branches = 0 +ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1 +ij_kotlin_block_comment_add_space = false +ij_kotlin_block_comment_at_first_column = true +ij_kotlin_call_parameters_new_line_after_left_paren = true +ij_kotlin_call_parameters_right_paren_on_new_line = true +ij_kotlin_call_parameters_wrap = on_every_item +ij_kotlin_catch_on_new_line = false +ij_kotlin_class_annotation_wrap = split_into_lines +ij_kotlin_continuation_indent_for_chained_calls = false +ij_kotlin_continuation_indent_for_expression_bodies = false +ij_kotlin_continuation_indent_in_argument_lists = false +ij_kotlin_continuation_indent_in_elvis = false +ij_kotlin_continuation_indent_in_if_conditions = false +ij_kotlin_continuation_indent_in_parameter_lists = false +ij_kotlin_continuation_indent_in_supertype_lists = false +ij_kotlin_else_on_new_line = false +ij_kotlin_enum_constants_wrap = off +ij_kotlin_extends_list_wrap = normal +ij_kotlin_field_annotation_wrap = split_into_lines +ij_kotlin_finally_on_new_line = false +ij_kotlin_function_context_parameters_wrap = split_into_lines +ij_kotlin_if_rparen_on_new_line = true +ij_kotlin_import_nested_classes = false +ij_kotlin_imports_layout = *, java.**, javax.**, kotlin.**, ^ +ij_kotlin_indent_before_arrow_on_new_line = true +ij_kotlin_insert_whitespaces_in_simple_one_line_method = true +ij_kotlin_keep_blank_lines_before_right_brace = 2 +ij_kotlin_keep_blank_lines_in_code = 2 +ij_kotlin_keep_blank_lines_in_declarations = 2 +ij_kotlin_keep_first_column_comment = true +ij_kotlin_keep_indents_on_empty_lines = false +ij_kotlin_keep_line_breaks = true +ij_kotlin_lbrace_on_next_line = false +ij_kotlin_line_break_after_multiline_when_entry = true +ij_kotlin_line_comment_add_space = false +ij_kotlin_line_comment_add_space_on_reformat = false +ij_kotlin_line_comment_at_first_column = true +ij_kotlin_method_annotation_wrap = split_into_lines +ij_kotlin_method_call_chain_wrap = normal +ij_kotlin_method_parameters_new_line_after_left_paren = true +ij_kotlin_method_parameters_right_paren_on_new_line = true +ij_kotlin_method_parameters_wrap = on_every_item +ij_kotlin_name_count_to_use_star_import = 5 +ij_kotlin_name_count_to_use_star_import_for_members = 3 +ij_kotlin_packages_to_use_import_on_demand = java.util.*, kotlinx.android.synthetic.**, io.ktor.** +ij_kotlin_parameter_annotation_wrap = off +ij_kotlin_property_context_parameters_wrap = split_into_lines +ij_kotlin_space_after_comma = true +ij_kotlin_space_after_extend_colon = true +ij_kotlin_space_after_type_colon = true +ij_kotlin_space_before_catch_parentheses = true +ij_kotlin_space_before_comma = false +ij_kotlin_space_before_extend_colon = true +ij_kotlin_space_before_for_parentheses = true +ij_kotlin_space_before_if_parentheses = true +ij_kotlin_space_before_lambda_arrow = true +ij_kotlin_space_before_type_colon = false +ij_kotlin_space_before_when_parentheses = true +ij_kotlin_space_before_while_parentheses = true +ij_kotlin_spaces_around_additive_operators = true +ij_kotlin_spaces_around_assignment_operators = true +ij_kotlin_spaces_around_elvis = true +ij_kotlin_spaces_around_equality_operators = true +ij_kotlin_spaces_around_function_type_arrow = true +ij_kotlin_spaces_around_logical_operators = true +ij_kotlin_spaces_around_multiplicative_operators = true +ij_kotlin_spaces_around_range = false +ij_kotlin_spaces_around_relational_operators = true +ij_kotlin_spaces_around_unary_operator = false +ij_kotlin_spaces_around_when_arrow = true +ij_kotlin_variable_annotation_wrap = off +ij_kotlin_while_on_new_line = false +ij_kotlin_wrap_elvis_expressions = 1 +ij_kotlin_wrap_expression_body_functions = 1 +ij_kotlin_wrap_first_method_in_call_chain = false + +[{*.markdown,*.md}] +ij_markdown_force_one_space_after_blockquote_symbol = true +ij_markdown_force_one_space_after_header_symbol = true +ij_markdown_force_one_space_after_list_bullet = true +ij_markdown_force_one_space_between_words = true +ij_markdown_format_tables = true +ij_markdown_insert_quote_arrows_on_wrap = true +ij_markdown_keep_indents_on_empty_lines = false +ij_markdown_keep_line_breaks_inside_text_blocks = true +ij_markdown_max_lines_around_block_elements = 1 +ij_markdown_max_lines_around_header = 1 +ij_markdown_max_lines_between_paragraphs = 1 +ij_markdown_min_lines_around_block_elements = 1 +ij_markdown_min_lines_around_header = 1 +ij_markdown_min_lines_between_paragraphs = 1 +ij_markdown_wrap_text_if_long = true +ij_markdown_wrap_text_inside_blockquotes = true + +[{*.pb,*.textproto,*.txtpb}] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 4 +ij_prototext_keep_blank_lines_in_code = 2 +ij_prototext_keep_indents_on_empty_lines = false +ij_prototext_keep_line_breaks = true +ij_prototext_space_after_colon = true +ij_prototext_space_after_comma = true +ij_prototext_space_before_colon = false +ij_prototext_space_before_comma = false +ij_prototext_spaces_within_braces = true +ij_prototext_spaces_within_brackets = false + +[{*.properties,spring.handlers,spring.schemas}] +ij_properties_align_group_field_declarations = false +ij_properties_keep_blank_lines = false +ij_properties_key_value_delimiter = equals +ij_properties_spaces_around_key_value_delimiter = false + +[{*.qute.htm,*.qute.html,*.qute.json,*.qute.txt,*.qute.yaml,*.qute.yml}] +ij_qute_keep_indents_on_empty_lines = false + +[{*.tf,*.tfvars}] +indent_size = 2 +ij_hcl-terraform_array_wrapping = normal +ij_hcl-terraform_import_providers_automatically = true +ij_hcl-terraform_keep_blank_lines_in_code = 2 +ij_hcl-terraform_keep_indents_on_empty_lines = false +ij_hcl-terraform_keep_line_breaks = true +ij_hcl-terraform_line_commenter_character = pound_sign_( #) +ij_hcl-terraform_object_wrapping = normal +ij_hcl-terraform_property_alignment = on_equals +ij_hcl-terraform_run_tf_fmt_on_reformat = true +ij_hcl-terraform_space_after_comma = true +ij_hcl-terraform_space_before_comma = false +ij_hcl-terraform_spaces_around_assignment_operators = true +ij_hcl-terraform_spaces_within_braces = false +ij_hcl-terraform_spaces_within_brackets = false +ij_hcl-terraform_wrap_long_lines = false + +[{*.toml,Cargo.lock,Cargo.toml.orig,Gopkg.lock,Pipfile,pdm.lock,poetry.lock,uv.lock}] +ij_toml_keep_indents_on_empty_lines = false + +[{*.yaml,*.yml}] +indent_size = 2 +ij_yaml_align_values_properties = do_not_align +ij_yaml_autoinsert_sequence_marker = true +ij_yaml_block_mapping_on_new_line = false +ij_yaml_indent_sequence_value = true +ij_yaml_keep_indents_on_empty_lines = false +ij_yaml_keep_line_breaks = true +ij_yaml_line_comment_add_space = false +ij_yaml_line_comment_add_space_on_reformat = false +ij_yaml_line_comment_at_first_column = true +ij_yaml_sequence_on_new_line = false +ij_yaml_space_before_colon = false +ij_yaml_spaces_within_braces = true +ij_yaml_spaces_within_brackets = true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 2e1c0dc..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,28 +0,0 @@ -# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time -# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle - -name: Build - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - cache: gradle - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - name: Build with Gradle - run: ./gradlew build diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e689a8f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,110 @@ +name: "CI" +on: + push: + branches: + - '[0-9]+.[0-9]+.x' + - 'main' + pull_request: + workflow_dispatch: +env: + JAVA_DISTRIBUTION: 'liberica' +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} +jobs: + build: + name: "Build & Test" + runs-on: ubuntu-24.04 + steps: + - name: "Output Agent IP" # in the event your agent has network issues, you can use this to debug + run: curl -s https://api.ipify.org + - name: "📥 Checkout repository" + uses: actions/checkout@v6 + - name: "Export .sdkmanrc properties" + uses: apache/grails-github-actions/export-gradle-properties@asf + with: + file: ".sdkmanrc" + prefix: "SDKMANRC_" + - name: "Determine Java Version" + run: echo "SDKMANRC_java=${{ env.SDKMANRC_java }}" | sed 's/-.*//' >> $GITHUB_ENV + - name: "☕️ Setup JDK" + uses: actions/setup-java@v5 + with: + distribution: ${{ env.JAVA_DISTRIBUTION }} + java-version: ${{ env.SDKMANRC_java }} + - name: 'Ensure Common Build Date' # to ensure a reproducible build + run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> "$GITHUB_ENV" + - name: "Export gradle.properties properties" + uses: apache/grails-github-actions/export-gradle-properties@asf + - name: "🐘 Setup Gradle" + uses: gradle/actions/setup-gradle@v5 + with: + build-scan-publish: ${{ env.ciBuildScanPublish }} + build-scan-terms-of-use-url: ${{ env.ciBuildScanTermsOfUseUrl }} + build-scan-terms-of-use-agree: ${{ env.ciBuildScanTermsOfUseAgree }} + - name: "🔨 Build project without tests" + if: ${{ contains(github.event.head_commit.message, '[skip tests]') }} + run: > + ./gradlew build + --continue + --stacktrace + -PskipTests + -PskipCodeStyle + - name: "🔨 Build project with tests" + if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }} + run: > + ./gradlew build + --continue + --stacktrace + --rerun-tasks + -PskipCodeStyle + publish: + # only run the publishing task on this repo (not on forks) + if: github.repository_owner == 'gpc' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') + needs: build + name: "Publish Snapshot" + runs-on: ubuntu-24.04 + steps: + - name: "Output Agent IP" # in the event your agent has network issues, you can use this to debug + run: curl -s https://api.ipify.org + - name: "📥 Checkout repository" + uses: actions/checkout@v6 + - name: "Export .sdkmanrc properties" + uses: apache/grails-github-actions/export-gradle-properties@asf + with: + file: ".sdkmanrc" + prefix: "SDKMANRC_" + - name: "Determine Java Version" + run: echo "SDKMANRC_java=${{ env.SDKMANRC_java }}" | sed 's/-.*//' >> $GITHUB_ENV + - name: "☕️ Setup JDK" + uses: actions/setup-java@v5 + with: + distribution: ${{ env.JAVA_DISTRIBUTION }} + java-version: ${{ env.SDKMANRC_java }} + - name: 'Ensure Common Build Date' # to ensure a reproducible build + run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> "$GITHUB_ENV" + - name: "Export gradle.properties properties" + uses: apache/grails-github-actions/export-gradle-properties@asf + - name: "🐘 Setup Gradle" + uses: gradle/actions/setup-gradle@v5 + with: + build-scan-publish: ${{ env.ciBuildScanPublish }} + build-scan-terms-of-use-url: ${{ env.ciBuildScanTermsOfUseUrl }} + build-scan-terms-of-use-agree: ${{ env.ciBuildScanTermsOfUseAgree }} + - name: "📤 Publish Snapshot Artifacts" + env: + GRAILS_PUBLISH_RELEASE: 'false' + MAVEN_PUBLISH_URL: 'https://central.sonatype.com/repository/maven-snapshots/' + MAVEN_PUBLISH_USERNAME: ${{ secrets.NEXUS_PUBLISH_USERNAME }} + MAVEN_PUBLISH_PASSWORD: ${{ secrets.NEXUS_PUBLISH_PASSWORD }} + run: > + ./gradlew publish + --no-build-cache + --rerun-tasks + - name: "📜 Generate Documentation" + run: ./gradlew docs + - name: "🚀 Publish to Github Pages" + uses: apache/grails-github-actions/deploy-github-pages@asf + env: + GRADLE_PUBLISH_RELEASE: 'false' + SOURCE_FOLDER: build/docs diff --git a/.github/workflows/release-notes.yml b/.github/workflows/release-notes.yml new file mode 100644 index 0000000..4951a17 --- /dev/null +++ b/.github/workflows/release-notes.yml @@ -0,0 +1,26 @@ +name: "Release - Drafter" +on: + issues: + types: [ closed,reopened ] + push: + branches: + - '[0-9]+.[0-9]+.x' + - 'main' + pull_request: + types: [ opened, reopened, synchronize, labeled ] + workflow_dispatch: +# queue jobs and only allow 1 run per branch due to the likelihood of hitting GitHub resource limits +concurrency: + group: release-pipeline + cancel-in-progress: false +jobs: + update_release_draft: + permissions: + contents: write # write permission is required to create a github release + pull-requests: write # write permission is required for auto-labeler + runs-on: ubuntu-24.04 + steps: + - name: "📝 Update Release Draft" + uses: release-drafter/release-drafter@v6 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9051e0e..ed65e82 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,90 +1,197 @@ -name: Release +name: "Release" on: - release: - types: [ published ] + release: + types: [ published ] +permissions: { } +env: + # To prevent throttling of the GitHub api, + # include the GitHub token in an environment variable + # since the build will check for it + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GRAILS_PUBLISH_RELEASE: 'true' + JAVA_DISTRIBUTION: 'liberica' + REPO_NAME: ${{ github.event.repository.name }} + RUN_ID: ${{ github.run_id }} + TAG: ${{ github.event.release.tag_name }} + VERSION: 'will be computed in each job' +concurrency: + group: release-pipeline + cancel-in-progress: false jobs: - release: - runs-on: ubuntu-latest + publish: + name: "Stage Jar Files" + permissions: + packages: read # pre-release workflow + contents: write # to create a release + issues: write # to modify milestones + runs-on: ubuntu-24.04 + steps: + - name: "📝 Establish release version" + run: echo "VERSION=${TAG#v}" >> "$GITHUB_ENV" + - name: "Output Agent IP" # in the event RAO blocks this agent, this can be used to debug it + run: curl -s https://api.ipify.org + - name: "📥 Checkout repository" + uses: actions/checkout@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ref: ${{ env.TAG }} + - name: 'Ensure Common Build Date' # to ensure a reproducible build + run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> "$GITHUB_ENV" + - name: "Ensure source files use common date" + run: | + find . -depth \( -type f -o -type d \) -exec touch -d "@${SOURCE_DATE_EPOCH}" {} + + - name: "🔐 Generate key file for artifact signing" env: - GIT_USER_NAME: ${{ secrets.GIT_USER_NAME }} - GIT_USER_EMAIL: ${{ secrets.GIT_USER_EMAIL }} - steps: - - name: Checkout repository - uses: actions/checkout@v2 - with: - token: ${{ secrets.GH_TOKEN }} - - uses: gradle/wrapper-validation-action@v1 - - name: Set up JDK - uses: actions/setup-java@v1 - with: - java-version: 8 - - name: Get latest release version number - id: get_version - uses: battila7/get-version-action@v2 - - name: Run pre-release - uses: micronaut-projects/github-actions/pre-release@master - with: - token: ${{ secrets.GITHUB_TOKEN }} - - name: Publish to Sonatype OSSRH - env: - SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} - SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} - SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} - SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }} - SIGNING_PASSPHRASE: ${{ secrets.SIGNING_PASSPHRASE }} - SECRING_FILE: ${{ secrets.SECRING_FILE }} - RELEASE_VERSION: ${{ steps.get_version.outputs.version-without-v }} - run: | - echo "${SECRING_FILE}" | base64 -d > "${GITHUB_WORKSPACE}/secring.gpg" - echo "Publishing Artifacts for $RELEASE_VERSION" - (set -x; ./gradlew -Pversion="${RELEASE_VERSION}" -Psigning.secretKeyRingFile="${GITHUB_WORKSPACE}/secring.gpg" publishToSonatype closeAndReleaseSonatypeStagingRepository --no-daemon) - - name: Bump patch version by one - uses: actions-ecosystem/action-bump-semver@v1 - id: bump_semver - with: - current_version: ${{steps.get_version.outputs.version-without-v }} - level: patch - - name: Set version in gradle.properties - env: - NEXT_VERSION: ${{ steps.bump_semver.outputs.new_version }} - run: | - echo "Preparing next snapshot" - ./gradlew snapshotVersion -Pversion="${NEXT_VERSION}" - - name: Commit & Push changes - uses: actions-js/push@master - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - author_name: ${{ secrets.GIT_USER_NAME }} - author_email: $${ secrets.GIT_USER_EMAIL }} - message: 'Set version to next SNAPSHOT' - - name: Check file documentation exists - id: check_documentation - uses: andstor/file-existence-action@v1 - with: - files: "src/docs" - - name: Build documentation - if: steps.check_documentation.outputs.files_exists == 'true' - env: - RELEASE_VERSION: ${{ steps.get_version.outputs.version-without-v }} - run: | - ./gradlew asciidoctor -Pversion="${RELEASE_VERSION}" - - name: Export Gradle Properties - uses: micronaut-projects/github-actions/export-gradle-properties@master - - name: Publish to Github Pages - if: steps.check_documentation.outputs.files_exists == 'true' && success() - uses: micronaut-projects/github-pages-deploy-action@master - env: - BETA: ${{ steps.get_version.outputs.isPrerelase }} - TARGET_REPOSITORY: ${{ github.repository }} - GH_TOKEN: ${{ secrets.GH_TOKEN }} - BRANCH: gh-pages - FOLDER: build/asciidoc - DOC_FOLDER: latest - COMMIT_EMAIL: ${{ secrets.GIT_USER_EMAIL }} - COMMIT_NAME: ${{ secrets.GIT_USER_NAME }} - VERSION: ${{ steps.get_version.outputs.version-without-v }} - - name: Run post-release - if: success() - uses: micronaut-projects/github-actions/post-release@master - with: - token: ${{ secrets.GITHUB_TOKEN }} + SECRING_FILE: ${{ secrets.SECRING_FILE }} + run: echo "$SECRING_FILE" | base64 -d > ${{ github.workspace }}/secring.gpg + - name: "Export .sdkmanrc properties" + uses: apache/grails-github-actions/export-gradle-properties@asf + with: + file: ".sdkmanrc" + prefix: "SDKMANRC_" + - name: "Determine Java Version" + run: echo "SDKMANRC_java=${{ env.SDKMANRC_java }}" | sed 's/-.*//' >> $GITHUB_ENV + - name: "☕️ Setup JDK" + uses: actions/setup-java@v5 + with: + distribution: ${{ env.JAVA_DISTRIBUTION }} + java-version: ${{ env.SDKMANRC_java }} + - name: "Export gradle.properties properties" + uses: apache/grails-github-actions/export-gradle-properties@asf + - name: "🐘 Setup Gradle" + uses: gradle/actions/setup-gradle@v5 + with: + build-scan-publish: ${{ env.ciBuildScanPublish }} + build-scan-terms-of-use-url: ${{ env.ciBuildScanTermsOfUseUrl }} + build-scan-terms-of-use-agree: ${{ env.ciBuildScanTermsOfUseAgree }} + - name: "⚙️ Run pre-release" + uses: apache/grails-github-actions/pre-release@asf + env: + RELEASE_VERSION: ${{ env.VERSION }} + - name: "📤 Publish to Maven Central" + env: + NEXUS_PUBLISH_USERNAME: ${{ secrets.NEXUS_PUBLISH_USERNAME }} + NEXUS_PUBLISH_PASSWORD: ${{ secrets.NEXUS_PUBLISH_PASSWORD }} + NEXUS_PUBLISH_URL: 'https://ossrh-staging-api.central.sonatype.com/service/local/' + NEXUS_PUBLISH_DESCRIPTION: '${{ env.REPO_NAME }}:${{ env.VERSION }}:${{ env.RUN_ID }}' + SIGNING_KEY: ${{ secrets.SIGNING_KEY }} + SIGNING_PASSPHRASE: ${{ secrets.SIGNING_PASSPHRASE }} + run: > + ./gradlew + -Psigning.secretKeyRingFile=${{ github.workspace }}/secring.gpg + publishMavenPublicationToSonatypeRepository + closeSonatypeStagingRepository + - name: "Generate Build Date file" + run: echo "$SOURCE_DATE_EPOCH" >> build/BUILD_DATE.txt + - name: "Upload Build Date file" + uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b + with: + files: build/BUILD_DATE.txt + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + release: + name: "Make Release Files Available" + environment: release # this step will be delayed until approved + needs: [ publish ] + runs-on: ubuntu-24.04 + permissions: + contents: read + steps: + - name: "📝 Establish release version" + run: echo "VERSION=${TAG#v}" >> "$GITHUB_ENV" + - name: "📥 Checkout repository" + uses: actions/checkout@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ref: ${{ env.TAG }} + - name: "Export .sdkmanrc properties" + uses: apache/grails-github-actions/export-gradle-properties@asf + with: + file: ".sdkmanrc" + prefix: "SDKMANRC_" + - name: "Determine Java Version" + run: echo "SDKMANRC_java=${{ env.SDKMANRC_java }}" | sed 's/-.*//' >> $GITHUB_ENV + - name: "☕️ Setup JDK" + uses: actions/setup-java@v5 + with: + distribution: ${{ env.JAVA_DISTRIBUTION }} + java-version: ${{ env.SDKMANRC_java }} + - name: "Export gradle.properties properties" + uses: apache/grails-github-actions/export-gradle-properties@asf + - name: "🐘 Setup Gradle" + uses: gradle/actions/setup-gradle@v5 + with: + build-scan-publish: ${{ env.ciBuildScanPublish }} + build-scan-terms-of-use-url: ${{ env.ciBuildScanTermsOfUseUrl }} + build-scan-terms-of-use-agree: ${{ env.ciBuildScanTermsOfUseAgree }} + - name: "📤 Release staging repository" + env: + NEXUS_PUBLISH_USERNAME: ${{ secrets.NEXUS_PUBLISH_USERNAME }} + NEXUS_PUBLISH_PASSWORD: ${{ secrets.NEXUS_PUBLISH_PASSWORD }} + NEXUS_PUBLISH_URL: 'https://ossrh-staging-api.central.sonatype.com/service/local/' + NEXUS_PUBLISH_DESCRIPTION: '${{ env.REPO_NAME }}:${{ env.VERSION }}:${{ env.RUN_ID }}' + run: > + ./gradlew + findSonatypeStagingRepository + releaseSonatypeStagingRepository + docs: + environment: docs # this step will be delayed until approved + name: "Publish Documentation" + needs: [ publish, release ] + runs-on: ubuntu-24.04 + permissions: + contents: write # required to publish documentation to GitHub pages branches + steps: + - name: "📝 Establish release version" + run: echo "VERSION=${TAG#v}" >> "$GITHUB_ENV" + - name: "📥 Checkout repository" + uses: actions/checkout@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ref: ${{ env.TAG }} + - name: "Export .sdkmanrc properties" + uses: apache/grails-github-actions/export-gradle-properties@asf + with: + file: ".sdkmanrc" + prefix: "SDKMANRC_" + - name: "Determine Java Version" + run: echo "SDKMANRC_java=${{ env.SDKMANRC_java }}" | sed 's/-.*//' >> $GITHUB_ENV + - name: "☕️ Setup JDK" + uses: actions/setup-java@v5 + with: + distribution: ${{ env.JAVA_DISTRIBUTION }} + java-version: ${{ env.SDKMANRC_java }} + - name: "Export gradle.properties properties" + uses: apache/grails-github-actions/export-gradle-properties@asf + - name: "🐘 Setup Gradle" + uses: gradle/actions/setup-gradle@v5 + with: + build-scan-publish: ${{ env.ciBuildScanPublish }} + build-scan-terms-of-use-url: ${{ env.ciBuildScanTermsOfUseUrl }} + build-scan-terms-of-use-agree: ${{ env.ciBuildScanTermsOfUseAgree }} + - name: "🔨 Build Documentation" + run: ./gradlew docs + - name: "🚀 Publish to Github Pages" + uses: apache/grails-github-actions/deploy-github-pages@asf + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GRADLE_PUBLISH_RELEASE: 'true' + SOURCE_FOLDER: build/docs + VERSION: ${{ env.VERSION }} + close: + name: "To Next Version" + environment: close # this step will be delayed until approved + needs: [ publish, docs, release ] + runs-on: ubuntu-24.04 + permissions: + contents: write # required for gradle.properties revert + issues: write # required for milestone closing + pull-requests: write # to create the PR that will increment the version + steps: + - name: "📥 Checkout repository" + uses: actions/checkout@v6 + with: + ref: ${{ env.TAG }} + token: ${{ secrets.GITHUB_TOKEN }} + - name: "⚙️ Run post-release" + uses: apache/grails-github-actions/post-release@asf diff --git a/.sdkmanrc b/.sdkmanrc new file mode 100644 index 0000000..03e409c --- /dev/null +++ b/.sdkmanrc @@ -0,0 +1,3 @@ +java=17.0.18-librca +gradle=8.14.4 +groovy=4.0.30 diff --git a/.skills/gradle-best-practices.md b/.skills/gradle-best-practices.md new file mode 100644 index 0000000..58f8693 --- /dev/null +++ b/.skills/gradle-best-practices.md @@ -0,0 +1,254 @@ +# Gradle Best Practices + +## Purpose + +This skill covers Gradle best practices for this project, including convention plugins, extension configuration, +lazy APIs, and build structure. Convention plugins remove duplication across subprojects by centralizing shared +build logic. They live in the `build-logic/` composite build and are applied by ID in each subproject's `build.gradle`. + +## Core Rules + +### NEVER configure subprojects from the root build.gradle + +The root `build.gradle` must NEVER use `subprojects {}`, `allprojects {}`, or `configure(subprojects.matching {...}) {}` +to apply plugins or configure subproject behavior. This is an antipattern that causes ordering issues, breaks project +isolation, and makes builds harder to reason about. + +```groovy +// BAD - Never do this in root build.gradle +subprojects { + apply plugin: 'groovy' + dependencies { + implementation 'org.example:shared-lib:1.0' + } +} + +// BAD - Never do this either +allprojects { + repositories { + mavenCentral() + } +} +``` + +Instead, create a convention plugin in `build-logic/` and apply it in each subproject that needs it: + +```groovy +// GOOD - build-logic/src/main/groovy/config.compile.gradle +plugins { + id 'groovy' +} +// shared compilation config here +``` + +```groovy +// GOOD - plugin/build.gradle +plugins { + id 'config.compile' +} +``` + +The ONLY exception is the `root-publish.gradle` convention plugin, which exists solely as a workaround for a Nexus +publishing bug (https://github.com/gradle-nexus/publish-plugin/issues/310) that requires version/group to be set at the +root level. + +### Use the composite build pattern + +Convention plugins reside in `build-logic/`, which is included as a composite build via `settings.gradle`: + +```groovy +pluginManagement { + includeBuild('./build-logic') { + it.name = 'build-logic' + } +} +``` + +### Naming convention + +Convention plugin files follow the pattern: + +``` +build-logic/src/main/groovy/config..gradle +``` + +The plugin ID matches the filename (minus the `.gradle` extension). For example: + +- `config.compile.gradle` -> plugin ID `config.compile` + +### Declare external plugin dependencies in build-logic/build.gradle + +When a convention plugin applies a third-party plugin, that plugin must be declared as an `implementation` dependency in +`build-logic/build.gradle`: + +```groovy +// build-logic/build.gradle +plugins { + id 'groovy-gradle-plugin' +} + +dependencies { + implementation platform("org.apache.grails:grails-bom:${gradleProperties.grailsVersion}") + implementation 'org.apache.grails:grails-gradle-plugins' + implementation "com.adarshr:gradle-test-logger-plugin:${gradleProperties.testLoggerVersion}" + implementation 'cloud.wondrify:asset-pipeline-gradle' + implementation 'org.apache.grails.gradle:grails-publish' +} +``` + +### Share properties from root gradle.properties + +The `build-logic/build.gradle` reads the root `gradle.properties` and exposes those values as extra properties so +convention plugins can reference them (e.g., `grailsVersion`): + +```groovy +file('../gradle.properties').withInputStream { is -> + extensions.extraProperties.set( + 'gradleProperties', + new Properties().tap { load(is) } + ) +} + +allprojects { project -> + gradleProperties.stringPropertyNames().each { key -> + project.extensions.extraProperties.set( + key, + gradleProperties.getProperty(key) + ) + } +} +``` + +## Avoid Eager Initialization + +Always use lazy/deferred APIs to avoid eagerly resolving tasks or configurations: + +```groovy +// GOOD - lazy task configuration +tasks.withType(JavaCompile).configureEach { + options.encoding = StandardCharsets.UTF_8.name() +} + +tasks.named('bootRun', JavaExec) { + doFirst { /* ... */ } +} + +tasks.register('docs') { + dependsOn(/* ... */) +} + +// BAD - eager resolution +tasks.withType(JavaCompile) { // missing .configureEach + options.encoding = 'UTF-8' +} + +task docs { // old task() API is eager + dependsOn /* ... */ +} +``` + +Key APIs to use: + +- `tasks.register()` instead of `task()` +- `tasks.named()` instead of `tasks.getByName()` +- `tasks.withType(X).configureEach {}` instead of `tasks.withType(X) {}` +- `project.provider {}` for lazy values +- `layout.buildDirectory` instead of `buildDir` +- `dependsOn()` method instead of `dependsOn =` setter (setter replaces all dependencies; the method adds to them) +- Do NOT chain `.configure {}` on `tasks.register()` or `tasks.named()` — pass the closure directly to preserve type hints + +## Extension Configuration with Type Hints + +When configuring project extensions (like publishing metadata or third-party plugin configurations), use +`extensions.configure(Type)` with explicit `it` for type hints and better IDE support: + +```groovy +// GOOD - explicit it in extensions.configure() for type hints +extensions.configure(GrailsPublishExtension) { + it.artifactId = project.name + it.githubSlug = 'gpc/taggable' + it.license.name = 'Apache-2.0' + it.title = 'My Plugin' + it.developers = [name: 'Developer Name'] +} +``` + +Explicit `it` is NOT required in `tasks.named()`, `tasks.register()`, or `configureEach` — these already have typed +delegates: + +```groovy +// GOOD - no explicit it needed, delegate is already typed +tasks.withType(Checkstyle).configureEach { + group = 'verification' + onlyIf { !project.hasProperty('skipCodeStyle') } +} + +tasks.named('bootRun', JavaExec) { + doFirst { + jvmArgs("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005") + } +} +``` + +**Benefits of `extensions.configure(Type)` with explicit `it`:** + +- IDE auto-completion and type-checking for extension properties +- Clearer intent: code readers immediately see the extension type being configured +- Reduces runtime errors from typos in property names + +## Composition Over Inheritance + +Convention plugins should compose by applying other convention plugins rather than duplicating logic: + +```groovy +// example.gradle applies other convention plugins +plugins { + id 'org.apache.grails.gradle.grails-web' + id 'org.apache.grails.gradle.grails-gsp' + id 'config.grails-assets' + id 'config.app-run' +} +``` + +## Existing Convention Plugins + +| Plugin | Purpose | +|----------------------------------|--------------------------------------------------------------------------------------| +| `app-run.gradle` | Debug flags for `bootRun` | +| `code-coverage.gradle` | JaCoCo coverage for project (XML + HTML reports) | +| `code-coverage-aggregate.gradle` | JaCoCo coverage aggregation across subprojects (XML + HTML reports) | +| `code-style.gradle` | Checkstyle + CodeNarc code style checking (configs in `build-logic/config/`) | +| `compile.gradle` | Java/Groovy compilation settings (UTF-8, incremental, Java release from `.sdkmanrc`) | +| `docs.gradle` | Documentation aggregation (Groovydoc + Asciidoctor) | +| `example-app.gradle` | Example app config (grails-web, GSP, assets) | +| `grails-assets.gradle` | Asset pipeline with Bootstrap/jQuery WebJars | +| `grails-plugin.gradle` | Grails plugin application | +| `publish.gradle` | Per-project Maven publishing metadata | +| `publish-root.gradle` | Root-level Nexus publishing workaround | +| `testing.gradle` | Test framework config (Spock, JUnit Platform, test-logger) | + +## When to Create a New Convention Plugin + +Create a new convention plugin when: + +- Two or more subprojects share the same build configuration +- A subproject's `build.gradle` grows beyond applying plugins and declaring dependencies +- You need to enforce a project-wide standard (e.g., code formatting, static analysis) + +Keep each convention plugin focused on a single concern. Prefer small, composable plugins over monolithic ones. + +## Repository Management + +Repositories are managed centrally in `settings.gradle` via `dependencyResolutionManagement`: + +```groovy +dependencyResolutionManagement { + repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS + repositories { + maven { url = 'https://repo.grails.org/grails/restricted' } + } +} +``` + +This prevents subprojects from declaring their own repositories, ensuring consistency. The `FAIL_ON_PROJECT_REPOS` mode +enforces this. diff --git a/.skills/plugin-project.md b/.skills/plugin-project.md new file mode 100644 index 0000000..956ef87 --- /dev/null +++ b/.skills/plugin-project.md @@ -0,0 +1,145 @@ +# Plugin Project Best Practices + +## Purpose + +The `plugin/` directory contains the Grails plugin artifact. It is the only publishable library in this repository. It +must contain ONLY the plugin source code and unit tests; nothing else. + +## Core Rules + +### Plugin project contains ONLY plugin code and unit tests + +The plugin project (`plugin/`) must contain: + +- Plugin source code under `src/main/groovy/` and `grails-app/` +- Unit tests under `src/test/groovy/` +- Plugin configuration files under `grails-app/conf/` + +The plugin project must NOT contain: + +- Integration tests (these belong in example apps under `examples/`) +- Functional tests (these belong in example apps under `examples/`) +- Example controllers, views, or domain classes +- Test controllers or test-specific artifacts +- Application-level configuration not related to the plugin (e.g., database config, asset config) + +### Why: separation of concerns + +Keeping integration/functional tests out of the plugin project ensures: + +1. The plugin artifact is clean – no test dependencies or test code leaks into the published JAR +2. Tests that require a running Grails application exercise the plugin as a real consumer would +3. The plugin's API surface is validated from the outside, not the inside +4. Different example apps can test different configurations of the plugin + +## Project Structure + +``` +plugin/ +├── build.gradle # Only convention plugins + dependencies +├── grails-app/ +│ ├── conf/ +│ │ ├── application.yml # Plugin-specific config defaults +│ ├── controllers/ # Interceptors, controller-scoped artifacts +│ │ └── org/grails/plugins/servertiming/ +│ │ └── ServerTimingInterceptor.groovy +└── src/ + ├── main/groovy/ # Core plugin classes + │ └── org/grails/plugins/servertiming/ + │ ├── ServerTimingAutoConfiguration.groovy + │ ├── ServerTimingFilter.groovy + │ ├── ServerTimingGrailsPlugin.groovy + │ ├── ServerTimingResponseWrapper.groovy + │ ├── config/ + │ │ ├── EnabledCondition.groovy + │ │ └── ServerTimingConfig.groovy + │ └── core/ + │ ├── Metric.groovy + │ └── TimingMetric.groovy + ├── main/resources/ + │ ├── META-INF/spring + │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports + │ └── spring-configuration-metadata.json + └── test/groovy/ # Unit tests ONLY + └── org/grails/plugins/servertiming/ + ├── MetricSpec.groovy + └── TimingMetricSpec.groovy +``` + +## build.gradle Pattern + +The plugin's `build.gradle` should be minimal -- apply convention plugins and declare dependencies: + +```groovy +plugins { + id 'config.compile' + id 'config.testing' + id 'config.grails-plugin' + id 'config.publish' +} + +version = projectVersion +group = 'io.github.gpc' + +dependencies { + + profile 'org.apache.grails.profiles:web-plugin' + console 'org.apache.grails:grails-console' + + compileOnly platform("org.apache.grails:grails-bom:$grailsVersion") + compileOnly 'org.apache.grails:grails-dependencies-starter-web' + + testImplementation platform("org.apache.grails:grails-bom:$grailsVersion") + testImplementation 'org.apache.grails:grails-dependencies-starter-web' + testImplementation 'org.apache.grails:grails-dependencies-test' +} +``` + +Key patterns: + +- Use `compileOnly` for framework dependencies the consuming application will provide +- Use `testImplementation` for test-only dependencies +- Apply `config.publish` to configure Maven publishing metadata +- NEVER add custom task configuration here – move it to a convention plugin + +## Unit Test Guidelines + +Unit tests in the plugin project test individual classes in isolation: + +- Test domain logic, validation, and data structures (e.g., `Tag`, `TagLink`) +- Test utility classes and traits (e.g., `Taggable`) +- Use Spock Framework with `@Unroll` for data-driven tests +- Do NOT start the Grails application context for unit tests +- Do NOT make HTTP requests in unit tests +- Do NOT test controller actions, interceptors, or filters end-to-end in the plugin project + +### What belongs in unit tests + +- `Tag` name constraints (uniqueness, blank, length) +- `TagLink` polymorphic mapping validation +- `Taggable` trait instance methods (`addTag`, `removeTag`, `parseTags`, etc.) when isolated +- `TaggableService` helpers that do not require a running context +- Equals/hashCode contracts on domain entities +- Validation error cases + +### What does NOT belong in unit tests + +- Testing that `Taggable` static methods query correctly against a real datastore (integration test) +- Testing that `TaggableService.refreshDomainClasses()` wires up domain class families in a running app (integration test) +- Testing `` rendering in a GSP view (functional test) +- Testing behavior with a real Hibernate session, GSP views, JSON rendering, or static assets (functional test) + +## Plugin Descriptor + +The `TaggableGrailsPlugin` class extends `grails.plugins.Plugin` and exposes important +information about the plugin to the Grails framework. + +## Dependency Scoping + +- **`compileOnly`**: Framework dependencies the host app provides (Grails web, servlet API) +- **`implementation`**: Dependencies the plugin bundles and needs at runtime (use sparingly) +- **`testImplementation`**: Test framework dependencies (Spock, grails-testing-support) +- **`console`**: Grails console support +- **`profile`**: The Grails profile (web-plugin for plugins) + +Avoid `implementation` for Grails/Spring/Servlet dependencies -- the consuming application provides these. diff --git a/.skills/repository-structure.md b/.skills/repository-structure.md new file mode 100644 index 0000000..c07cbb8 --- /dev/null +++ b/.skills/repository-structure.md @@ -0,0 +1,216 @@ +# Grails Plugin Repository Structure + +## Purpose + +This document defines the canonical structure for Grails plugin repositories. The structure enforces separation of +concerns: the plugin project contains only library code and unit tests, example apps provide integration/functional test +coverage, and build logic is centralized in convention plugins. + +## Directory Layout + +``` +taggable/ +├── .github/ # CI/CD workflows and GitHub config +│ ├── workflows/ +│ │ ├── ci.yml # Build, test, publish snapshots +│ │ ├── code-coverage.yml # Create a code coverage report +│ │ ├── code-style.yml # Check code style +│ │ ├── release.yml # Multi-stage release pipeline +│ │ └── release-notes.yml # Automated release draft notes +│ ├── release-drafter.yml # Release drafter categories/labels +│ └── dependency-graph/ +│ └── external-references.yml # Maven Central package association +│ +├── build-logic/ # Gradle convention plugins (composite build) +│ ├── build.gradle # Plugin dependencies (groovy-gradle-plugin) +│ ├── settings.gradle # Build-logic project settings +│ ├── config/ # Shared code style config files +│ │ ├── checkstyle/ # Checkstyle XML configs +│ │ └── codenarc/ # CodeNarc ruleset +│ └── src/main/groovy/ # Convention plugin files (*.gradle) +│ ├── config.app-run.gradle +│ ├── config.code-coverage.gradle +│ ├── config.code-coverage-aggregate.gradle +│ ├── config.code-style.gradle +│ ├── config.compile.gradle +│ ├── config.docs.gradle +│ ├── config.example-app.gradle +│ ├── config.grails-assets.gradle +│ ├── config.grails-plugin.gradle +│ ├── config.publish.gradle +│ ├── config.publish-root.gradle +│ └── config.testing.gradle +│ +├── plugin/ # The Grails plugin artifact +│ ├── build.gradle # Convention plugins + dependencies only +│ ├── grails-app/ +│ │ ├── conf/ # Plugin config (application.yml, logback) +│ │ └── controllers/ # Interceptors and controller artifacts +│ └── src/ +│ ├── main/groovy/ # Plugin source code +│ └── test/groovy/ # Unit tests ONLY +│ +├── examples/ # Example apps (auto-discovered) +│ ├── app1/ # first example app with the plugin enabled +│ │ ├── build.gradle +│ │ ├── grails-app/ # Standard Grails app structure +│ │ │ ├── conf/ +│ │ │ ├── controllers/ # Test controllers +│ │ │ ├── views/ # Test views (GSP) +│ │ │ ├── init/ +│ │ │ ├── assets/ +│ │ │ └── i18n/ +│ │ └── src/ +│ │ └── integration-test/ # Integration & functional tests +│ └── app2/ # second app showing disable feature +│ ├── build.gradle +│ ├── grails-app/ # Standard Grails app structure +│ │ ├── conf/ +│ │ ├── controllers/ # Test controllers +│ │ ├── views/ # Test views (GSP) +│ │ ├── init/ +│ │ ├── assets/ +│ │ └── i18n/ +│ └── src/ +│ └── integration-test/ # Integration & functional tests +│ +├── code-coverage/ # JaCoCo coverage aggregation +│ └── build.gradle # Declares which projects contribute coverage data +│ +├── docs/ # Asciidoctor documentation +│ ├── build.gradle +│ └── src/docs/ # .adoc source files +│ +├── build.gradle # Root build (docs + root-publish ONLY) +├── settings.gradle # Multi-project settings + composite build +├── gradle.properties # Shared version properties +├── .sdkmanrc # SDK versions (Java, Gradle, Groovy) +├── AGENTS.md # AI agent instructions +├── .skills/ # Best practice skill files +├── LICENSE # Apache 2.0 +└── README.md +``` + +## Key Architectural Rules + +### 1. Root build.gradle is minimal + +The root `build.gradle` applies only root-level convention plugins (docs aggregation, root-publish workaround). It must +NEVER use `subprojects {}`, `allprojects {}`, or any mechanism to configure child projects. All shared configuration +flows through convention plugins. + +```groovy +// Root build.gradle -- this is all that should be here +plugins { + id 'idea' + id 'config.docs' + id 'config.root-publish' +} +``` + +### 2. Plugin project = library code + unit tests + +The `plugin/` project is the published artifact. It contains: + +- Source code (`src/main/groovy/`, `grails-app/`) +- Unit tests (`src/test/groovy/`) + +It does NOT contain integration tests, functional tests, example controllers, or test views. + +### 3. Example apps = integration/functional tests + +All tests requiring a running Grails application live in example apps under `examples/`. Each app: + +- Depends on the plugin via `implementation project(':taggable')` +- Contains test controllers and views that exercise the plugin +- Contains integration tests under `src/integration-test/` +- Is auto-discovered by `settings.gradle` + +### 4. Build logic is centralized + +Convention plugins in `build-logic/` eliminate all duplication: + +- Compilation settings: `config.compile.gradle` +- Test configuration: `config.testing.gradle` +- Plugin setup: `config.grails-plugin.gradle` +- Example app setup: `config.example-app.gradle` +- Publishing: `config.publish.gradle` +- Coverage aggregation: `config.coverage-aggregate.gradle` +- Code style checking: `config.code-style.gradle` + +### 5. Centralized dependency resolution + +Repositories are declared once in `settings.gradle` using `dependencyResolutionManagement`. The `FAIL_ON_PROJECT_REPOS` +mode prevents subprojects from declaring their own repositories. + +### 6. Shared properties via gradle.properties + +Version numbers and shared settings live in `gradle.properties` at the root: + +```properties +projectVersion=0.0.1-SNAPSHOT +grailsVersion=7.0.7 +``` + +These are available in all subprojects as project properties (`projectVersion`, `grailsVersion`). + +## Adding a New Example App + +1. Create a new directory under `examples/` (e.g., `examples/app2/`) +2. Add a `build.gradle` applying the convention plugins: + ```groovy + plugins { + id 'config.example-app' + } + ``` +3. Add standard Grails app structure under `grails-app/` +4. Add integration tests under `src/integration-test/groovy/` +5. The app will be auto-discovered by `settings.gradle` and automatically included in coverage aggregation -- no manual + registration needed + +## Adding a New Convention Plugin + +1. Create a new file: `build-logic/src/main/groovy/config..gradle` +2. If the plugin applies third-party plugins, add their dependencies to `build-logic/build.gradle` +3. Apply the new plugin ID in the relevant subproject(s) +4. Keep the plugin focused on a single concern + +## Build Commands + +```bash +# Full build (all subprojects) +./gradlew build + +# Plugin unit tests only +./gradlew :taggable:test + +# Example app integration tests +./gradlew :app1:integrationTest + +# Aggregated coverage report (unit + integration) +./gradlew jacocoAggregatedReport + +# Run an example app +./gradlew :app1:bootRun + +# Generate documentation +./gradlew docs + +# Clean everything +./gradlew clean + +# Skip tests +./gradlew build -PskipTests +``` + +## SDK Management + +The `.sdkmanrc` file pins exact SDK versions. Run `sdk env install` to install them. CI reads `.sdkmanrc` to determine +the Java version dynamically. + +## CI/CD Pipeline + +- **CI**: Builds and tests on every push/PR. Publishes snapshots on push to release branches. +- **Coverage**: Runs the full build and posts an aggregated JaCoCo coverage summary to the GitHub Actions job summary. +- **Release**: 4-stage pipeline (stage -> release -> docs -> version bump) triggered by GitHub release. +- **Release Notes**: Auto-drafts release notes from PRs using release-drafter with category labels. diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 48b31d6..0000000 --- a/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -language: groovy -jdk: -- oraclejdk7 -before_script: -- rm -rf target -script: ./travis-build.sh -env: - global: - - GIT_NAME="Graeme Rocher" - - GIT_EMAIL="graeme.rocher@gmail.com" - - secure: nDFWKPneybRf6eS5g/UimKxizX7Z/EyJgq6Zz++OIfENuLTK5ZfXNgeCB0E3CVQ/EsO1pZIi6rJz27s9sDeneBgmBdoKu4hAIVHk5jwL26SKFd1G3zy9+JHEGrYlgC4pcxKydWuhm7euqJT2ighuSlSIPjcSR9ep+87moHLv3NY= - - secure: MdCohHHlR+BT0YSkyqddr7ejNCwWNeg0epuPkCqoFRczOdNnT+TGeWrhxMzQVYPNpe2QG76sqrWwyGtJRAn6/Gch7bYfpHkoGWWsacm2VGXLPXucjyz2vVg+ZzWGirkssFfFVJsG33mljzhxjljGviKDgsVS2DYwRBO9OKQYqXY= - - secure: QOozG0LmSnIeslpKao6iudVCZ/MLGoI1nkELLDMer1EkZMOOD2jjWuTI1OvGeD2RKzeS2e78iyvXm5ohSSoAVqZ+IAKdG0SQWw/vXNUJV1PkgPO57DNnp5iyGwVh9vK+Q+iUy9veoRiaX91aWL5JzXobjHOxf7YcCjnYMZ/fTuU= - - secure: DOUGG6+sqQWYJlc8oa2Faio2lgMhU+gOo0eAGpn20omf0v/+4Wa/YqmaqW77TM0bRxtx0Qt1S8f2R4RfSum2M58Kn5FuNJPOFSycpRdUv7A5tmJ+aNtCUpx2oBOp4q31exQH4K3KPRidVE7DQDMPX7KkvvLD4G9C6V6qZUQ4SAY= - - secure: Pr2loG4v2x4xH/+K8NImt+Geo3wT7chY4tiCOXWnuqID/jU2CIRoEpk8df3YS3S2MtStnDEMMDWgMx+3rfCURMEGlr9Nd7z5XkukcgnWpbB4YEm2vx/B37cGhGmbeGJdQTanPvcG8HyoJuBlkhUEAFJ6kNtfcUZWnmtIj7/EfCM= -sudo: false -cache: - directories: - - $HOME/.gradle diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..29a9231 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,202 @@ +# AGENTS.md - taggable + +## Project Overview + +This is a **Grails Plugin** that adds a generic tagging mechanism to any Grails domain class. Domain classes implement +the `Taggable` trait to gain instance and static methods for attaching, querying, and counting tags, plus a taglib for +rendering a frequency-ranked tag cloud. + +- **Language:** Groovy 4.0.30 on Java 17 +- **Framework:** Grails 7.x +- **Build System:** Gradle 8.14.4 (with wrapper) +- **Current Version:** 7.0.x-SNAPSHOT +- **License:** Apache 2.0 + +## Skill Files (Best Practices) + +Detailed best practices are documented in `.skills/`: + +| Skill File | Purpose | +|------------------------------------------------------------------------|-------------------------------------------------------| +| [`.skills/repository-structure.md`](.skills/repository-structure.md) | Canonical directory layout and architectural rules | +| [`.skills/gradle-best-practices.md`](.skills/gradle-best-practices.md) | Gradle best practices, convention plugins, and idioms | +| [`.skills/plugin-project.md`](.skills/plugin-project.md) | Plugin project scope: source code + unit tests only | +| [`.skills/example-apps.md`](.skills/example-apps.md) | Example app patterns: integration & functional tests | + +**Read these skill files before making structural changes to the repository.** + +## Critical Rules + +1. **NEVER add code to the root `build.gradle` to configure subprojects.** No `subprojects {}`, `allprojects {}`, or + `configure()` blocks. All shared configuration goes through convention plugins in `build-logic/`. +2. **The plugin project contains ONLY plugin code and unit tests.** No integration tests, no functional tests, no + example controllers or views. +3. **Example apps under `examples/` host all integration and functional tests.** They depend on the plugin via + `implementation project(':taggable')` and test it as a real consumer would. +4. **Use Gradle convention plugins to deduplicate.** If two or more subprojects share build logic, extract it into a + convention plugin in `build-logic/`. +5. **Always use lazy Gradle APIs** to avoid eager initialization (`tasks.register()`, `tasks.named()`, `configureEach`, + `provider {}`). + +## Repository Structure + +``` +taggable/ +├── .skills/ # Best practice skill files +├── plugin/ # Core Grails plugin (artifact: taggable) +│ ├── grails-app/ # Plugin domain classes, services, and taglibs +│ └── src/main/ # Plugin source code (Taggable trait, plugin descriptor) +├── examples/app1/ # Example Grails app +│ └── grails-app/ # Domain classes and conf for integration testing +├── docs/ # Asciidoctor documentation +├── build-logic/ # Gradle convention plugins (composite build) +├── .github/workflows/ # CI, release, and release-notes workflows +├── build.gradle # Root build file (docs + root-publish ONLY) +├── settings.gradle # Multi-project settings +└── gradle.properties # Version properties +``` + +## Build and Test Commands + +```bash +# Full build (compile + test) +./gradlew build + +# Run only unit tests (plugin module) +./gradlew :taggable:test + +# Run integration tests (example app) +./gradlew :app1:integrationTest + +# Skip tests +./gradlew build -PskipTests + +# Run the example app +./gradlew :app1:bootRun + +# Generate documentation +./gradlew docs + +# Clean build +./gradlew clean build + +# Run code style checks only +./gradlew codeStyle + +# Skip code style checks +./gradlew build -PskipCodeStyle +``` + +## SDK Requirements + +Use SDKMAN to install the correct tool versions (see `.sdkmanrc`): + +- Java: `17.0.18-librca` +- Gradle: `8.14.4` +- Groovy: `4.0.30` + +Run `sdk env install` to set up the environment. + +## Architecture + +The plugin provides a trait-based tagging mechanism with a supporting service, domain model, and tag library: + +1. **`TaggableGrailsPlugin`** is the plugin descriptor. It extends `grails.plugins.Plugin`, observes `hibernate`, and + calls `taggableService.refreshDomainClasses()` on startup and `onChange` so the domain class family map stays in + sync with reloaded artifacts. +2. **`Taggable`** is a Groovy trait that domain classes implement to become taggable. It provides instance methods + like `addTag(name)`, `addTags(names)`, `removeTag(name)`, `setTags(list)`, `parseTags(str, delimiter)`, and + `getTags()`, plus static methods `findAllByTag(name)`, `findAllByTagWithCriteria(name, closure)`, + `countByTag(name)`, `getAllTags()`, `getTotalTags()`, and `findAllTagsWithCriteria(params, closure)`. +3. **`TaggableService`** maintains `domainClassFamilies` — a map from class name to the property names of that class + plus all of its subclasses — so polymorphic queries against `TagLink` can include subclass references. It exposes + `getTagCounts(type)` for aggregation and `refreshDomainClasses()` which is called by the plugin lifecycle. +4. **`Tag`** and **`TagLink`** are the domain entities. `Tag` stores the unique tag name; `TagLink` is the polymorphic + join table with `tag`, `tagRef` (the id of the tagged instance), and `type` (the simple class name of the tagged + domain class). +5. **`TagsTagLib`** (namespace `tags`) provides `` for rendering a frequency-ranked tag cloud where CSS + class names scale with tag frequency. + +### Core Classes + +| Class / Interface | Location | Purpose | +|-------------------------|-------------------------------------------------------|--------------------------------------------------------------------| +| `TaggableGrailsPlugin` | `plugin/src/main/groovy/grails/plugins/taggable/` | Plugin descriptor; refreshes domain class families on load/change | +| `Taggable` | `plugin/src/main/groovy/grails/plugins/taggable/` | Trait that adds tagging methods to a domain class | +| `TagException` | `plugin/src/main/groovy/grails/plugins/taggable/` | Thrown when a tag is in an invalid state | +| `Tag` | `plugin/grails-app/domain/grails/plugins/taggable/` | Domain entity storing the unique tag name | +| `TagLink` | `plugin/grails-app/domain/grails/plugins/taggable/` | Polymorphic join entity (`tag`, `tagRef`, `type`) | +| `TaggableService` | `plugin/grails-app/services/grails/plugins/taggable/` | Maintains `domainClassFamilies`; exposes `getTagCounts(type)` | +| `TagsTagLib` | `plugin/grails-app/taglib/grails/plugins/taggable/` | Tag library: `` | + +## Configuration + +The plugin reads the following keys from the application config (for example, `application.yml` or `application.groovy`): + +| Key | Type | Default | Purpose | +|------------------------------------|--------------|--------------------------------------------------------|-----------------------------------------------------------------| +| `grails.taggable.preserve.case` | boolean | `false` | When `false`, tag names are normalized to lowercase | +| `grails.taggable.tag.table` | string | (Hibernate default) | Overrides the physical table name for `Tag` | +| `grails.taggable.tagLink.table` | string | (Hibernate default) | Overrides the physical table name for `TagLink` | +| `grails.taggable.tag.autoImport` | boolean | (Hibernate default) | Toggle Hibernate `autoImport` for `Tag` | +| `grails.taggable.tagLink.autoImport` | boolean | (Hibernate default) | Toggle Hibernate `autoImport` for `TagLink` | +| `grails.taggable.css.classes` | list | `['smallest','small','medium','large','largest']` | CSS classes used by `` to scale by frequency | + +Example `application.groovy`: + +```groovy +grails { + taggable { + preserve.case = false + css.classes = ['tag-xs', 'tag-sm', 'tag-md', 'tag-lg', 'tag-xl'] + tag.table = 'my_tag' + tagLink.table = 'my_tag_link' + } +} +``` + +## Testing + +### Unit Tests (`plugin/src/test/`) + +Unit tests use the **Spock Framework** and run on JUnit Platform. + +### Integration / Functional Tests (`examples/app1/`) + +The example app under `examples/app1/` declares taggable domain classes and exercises the `Taggable` trait, the +`TaggableService`, and the `` taglib against a real datastore. Integration and functional tests added +here depend on the plugin as a real consumer would. + +## Build-Logic Convention Plugins + +Convention plugins in `build-logic/src/main/groovy/` standardize build configuration: + +| Plugin | Purpose | +|--------------------------|--------------------------------------------------------------------------------------| +| `app-run.gradle` | Debug flags for `bootRun` | +| `compile.gradle` | Java/Groovy compilation settings (UTF-8, incremental, Java release from `.sdkmanrc`) | +| `docs.gradle` | Documentation aggregation (Groovydoc + Asciidoctor) | +| `example-app.gradle` | Example app config (grails-web, GSP, assets) | +| `grails-assets.gradle` | Asset pipeline with Bootstrap/jQuery WebJars | +| `grails-plugin.gradle` | Grails plugin application | +| `publish.gradle` | Per-project Maven publishing metadata | +| `publish-root.gradle` | Root-level Nexus publishing workaround | +| `testing.gradle` | Test framework config (Spock, JUnit Platform, test-logger) | + +## CI/CD + +- **CI** (`.github/workflows/ci.yml`): Builds and tests on push/PR; publishes snapshots to Maven Central Snapshots on + push to release branches. +- **Release** (`.github/workflows/release.yml`): 4-stage pipeline triggered by GitHub release — stage artifacts, release + to Maven Central, publish docs to GitHub Pages, bump version. +- **Release Notes** (`.github/workflows/release-notes.yml`): Auto-drafts release notes using release-drafter with + category labels. + +## Code Conventions + +- Groovy source files use standard Grails conventions (services, domain classes, and taglibs in `grails-app/`, other + classes in `src/main/groovy/`). +- **Use `def` for local variables** where the type is inferred from the right-hand side (e.g., constructor calls, + method calls, casts, factory methods). Explicit types should only be used for local variables when the type cannot + be inferred or when needed for `@CompileStatic` compilation. This applies to both production code and tests. +- When writing Gradle, always use the latest best practices to avoid eager initialization. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..129720c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,163 @@ +# Contributing to Taggable + +Thank you for your interest in contributing! This guide will help you get started. + +## Code of Conduct + +This project has a [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold it. Please report +unacceptable behavior to the project maintainers. + +## Getting Started + +### Prerequisites + +Install [SDKMAN!](https://sdkman.io/) to manage JDK, Gradle, and Groovy versions: + +```bash +curl -s "https://get.sdkman.io" | bash +``` + +### Setting Up the Development Environment + +```bash +# Clone the repository +git clone https://github.com/gpc/taggable.git +cd taggable + +# Install the required SDK versions (Java 17, Gradle 8.14.4, Groovy 4.0.30) +sdk env install + +# Build the project +./gradlew build +``` + +### Project Structure + +``` +taggable/ +├── plugin/ # The publishable Grails plugin (source + unit tests ONLY) +├── examples/app1/ # Example app with integration tests +├── build-logic/ # Gradle convention plugins (shared build configuration) +├── docs/ # Asciidoctor documentation +└── .skills/ # AI agent best-practice docs +``` + +Key architectural rules: + +- **Plugin module** contains only plugin source code and unit tests – no integration tests, no example controllers. +- **Example apps** under `examples/` host all integration and functional tests. They depend on the plugin as a real + consumer would. +- **Convention plugins** in `build-logic/` deduplicate build configuration. Never use `subprojects {}`, + `allprojects {}`, or `configure()` blocks in the root `build.gradle`. + +## Building and Testing + +```bash +# Full build (compile + all tests) +./gradlew build + +# Plugin unit tests only +./gradlew :taggable:test + +# Integration tests (runs the example app) +./gradlew :app1:integrationTest + +# Run the example app locally +./gradlew :app1:bootRun + +# Generate documentation +./gradlew docs + +# Skip tests +./gradlew build -PskipTests + +# Clean build +./gradlew clean build +``` + +### Code Coverage + +The project uses JaCoCo to aggregate coverage data from both plugin unit tests and example app integration tests. + +```bash +# Generate the aggregated coverage report +./gradlew jacocoAggregatedReport +``` + +Reports are generated at: + +| Report | Location | +|---------------------------------|----------------------------------------------------------------------------------| +| Aggregated (unit + integration) | `code-coverage/build/reports/jacoco/jacocoAggregatedReport/html/index.html` | +| Plugin unit tests | `plugin/build/reports/jacoco/test/html/index.html` | +| App1 integration tests | `examples/app1/build/reports/jacoco/jacocoIntegrationTestReport/html/index.html` | + +The aggregated report is also produced automatically as part of `./gradlew build`, so you can view it after any full +build. + +## Making Changes + +### Branching Strategy + +- Create a feature branch from the current release branch (e.g., `7.0.x`): + - `feature/short-description` for new features + - `fix/short-description` for bug fixes + - `docs/short-description` for documentation changes + - `refactor/short-description` for refactoring + +These branch prefixes are used by [release-drafter](https://github.com/release-drafter/release-drafter) to automatically +categorize changes in release notes. + +### Coding Standards + +- **Language:** Groovy 4.0 on Java 17 +- **Framework:** Grails 7.0 +- **Testing:** Spock Framework on JUnit Platform +- Follow existing code conventions in the project +- Keep the `Taggable` trait API backwards compatible — applications rely on the instance and static methods it adds to + their domain classes +- Preserve polymorphic `TagLink` semantics: any change to how `type` / `tagRef` is resolved must be reflected in + `TaggableService.domainClassFamilies` + +### Gradle Conventions + +- Always use lazy APIs: `tasks.register()`, `tasks.named()`, `configureEach`, `provider {}` +- Never use eager task creation (`tasks.create()`, `project.task()`) +- If two or more subprojects share build logic, extract it into a convention plugin in `build-logic/` + +## Submitting a Pull Request + +1. **Ensure all tests pass** locally: `./gradlew build` +2. **Write tests** for new functionality: + - Unit tests go in `plugin/src/test/` + - Integration tests go in `examples/app1/src/integration-test/` +3. **Update documentation** if you changed behavior or added features (in `docs/src/docs/`) +4. **Push your branch** and open a pull request against the release branch +5. **Fill out the PR template** completely + +### What to Expect + +- CI will run automatically on your PR +- A maintainer will review your changes +- You may be asked to make revisions +- Once approved, a maintainer will merge your PR + +## Reporting Issues + +- Use + the [bug report template](https://github.com/gpc/taggable/issues/new?template=bug_report.yml) + for bugs +- Use + the [feature request template](https://github.com/gpc/taggable/issues/new?template=feature_request.yml) + for enhancements +- Check [existing issues](https://github.com/gpc/taggable/issues) before creating a new one + +## Security Vulnerabilities + +If you discover a security vulnerability, **do not open a public issue**. Please see [SECURITY.md](.github/SECURITY.md) +for responsible disclosure instructions. + +## License + +By contributing to this project, you agree that your contributions will be licensed under +the [Apache License 2.0](LICENSE). diff --git a/README.md b/README.md index 67f0fdf..4f40c96 100644 --- a/README.md +++ b/README.md @@ -15,18 +15,17 @@ Classes can be made taggable by implementing the [grails.plugins.taggable.Taggab Requirements ------------ -Grails Version: 4.0.0 +Grails 7.0.x, Groovy 4.0.x, Java 17+ Installation ------------ -Add this dependency to `build.gradle` +Add this dependency to `build.gradle`: ```groovy - dependencies { - compile "io.github.gpc:taggable:4.0.0" +dependencies { + implementation 'io.github.gpc:taggable:7.0.0' } - ``` By default, the plugin will force all tags to lower case. If you want to preserve the case of tags, you must specify the diff --git a/build-logic/build.gradle b/build-logic/build.gradle new file mode 100644 index 0000000..746607d --- /dev/null +++ b/build-logic/build.gradle @@ -0,0 +1,29 @@ +plugins { + id 'groovy-gradle-plugin' +} + +file('../gradle.properties').withInputStream { is -> + extensions.extraProperties.set( + 'gradleProperties', + new Properties().tap { load(is) } + ) +} + +allprojects { project -> + gradleProperties.stringPropertyNames().each { key -> + project.extensions.extraProperties.set( + key, + gradleProperties.getProperty(key) + ) + } +} + +dependencies { + implementation platform("org.apache.grails:grails-bom:${gradleProperties.grailsVersion}") + implementation 'cloud.wondrify:asset-pipeline-gradle' + implementation "com.adarshr:gradle-test-logger-plugin:${gradleProperties.testLoggerVersion}" + implementation 'org.apache.grails:grails-gradle-plugins' + implementation 'org.apache.grails.gradle:grails-publish' + implementation "org.asciidoctor.jvm.convert:org.asciidoctor.jvm.convert.gradle.plugin:${gradleProperties.asciidoctorVersion}" +} + diff --git a/build-logic/settings.gradle b/build-logic/settings.gradle new file mode 100644 index 0000000..d712464 --- /dev/null +++ b/build-logic/settings.gradle @@ -0,0 +1,13 @@ +import org.gradle.api.initialization.resolve.RepositoriesMode + +rootProject.name = 'build-logic' + +dependencyResolutionManagement { + repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS + repositories { + if (System.getenv('INCLUDE_MAVEN_LOCAL')) { + mavenLocal() + } + maven { url = 'https://repo.grails.org/grails/restricted' } + } +} diff --git a/build-logic/src/main/groovy/config.app-run.gradle b/build-logic/src/main/groovy/config.app-run.gradle new file mode 100644 index 0000000..8363425 --- /dev/null +++ b/build-logic/src/main/groovy/config.app-run.gradle @@ -0,0 +1,12 @@ +pluginManager.withPlugin('org.springframework.boot') { + tasks.named('bootRun', JavaExec) { + doFirst { + if (project.hasProperty('debugWait')) { + jvmArgs('-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005') + } + if (project.hasProperty('debug')) { + jvmArgs('-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005') + } + } + } +} diff --git a/build-logic/src/main/groovy/config.compile.gradle b/build-logic/src/main/groovy/config.compile.gradle new file mode 100644 index 0000000..457667b --- /dev/null +++ b/build-logic/src/main/groovy/config.compile.gradle @@ -0,0 +1,65 @@ +import java.nio.charset.StandardCharsets + +plugins { + id 'groovy' +} + +tasks.withType(JavaCompile).configureEach { + options.with { + compilerArgs.add('-parameters') + encoding = StandardCharsets.UTF_8.name() + fork = true + incremental = true + release.set(resolveSdkmanJavaMajor(project)) + } + options.forkOptions.with { + jvmArgs.add('-Xmx1g') + memoryMaximumSize = '1g' + } +} + +tasks.withType(GroovyCompile).configureEach { + options.with { + compilerArgs.add('-parameters') + encoding = StandardCharsets.UTF_8.name() + fork = true + incremental = true + } + groovyOptions.with { + encoding = StandardCharsets.UTF_8.name() + optimizationOptions.indy = false + parameters = true + } + groovyOptions.forkOptions.with { + memoryMaximumSize = '1g' + jvmArgs.add('-Xmx1g') + } +} + +private static Provider resolveSdkmanJavaMajor(Project project) { + project.providers.provider { + def sdkmanrc = project.rootProject.file('.sdkmanrc') + if (!sdkmanrc.exists()) { + throw new GradleException('Missing .sdkmanrc in root project') + } + + def props = new Properties() + sdkmanrc.withInputStream { props.load(it) } + + def raw = props.getProperty('java')?.trim() + if (!raw) { + throw new GradleException('Missing java version in root project .sdkmanrc') + } + + def major = raw.tokenize('.').first() + if (!(major ==~ /\d+/)) { + throw new GradleException( + "Invalid java version '$raw' in root project .sdkmanrc (major '$major' is not an integer)" + ) + } + + return major.toInteger() + + } as Provider +} + diff --git a/build-logic/src/main/groovy/config.docs.gradle b/build-logic/src/main/groovy/config.docs.gradle new file mode 100644 index 0000000..712bff8 --- /dev/null +++ b/build-logic/src/main/groovy/config.docs.gradle @@ -0,0 +1,110 @@ +def docProject = provider { + project(":${project.name - 'root'}docs") +} +def pluginProject = provider { + project(":${project.name - '-root'}") +} + +tasks.register('cleanDocs', Delete) { + description = 'Deletes the documentation output' + group = 'documentation' + + delete(rootProject.layout.projectDirectory.dir('build/docs')) +} + +tasks.register('aggregateGroovyApiDoc', Groovydoc) { + description = 'Generates Groovy API Documentation for the plugin project under build/docs/gapi' + group = 'documentation' + + def upstream = pluginProject.flatMap { + it.tasks.named('groovydoc', Groovydoc) + } as Provider + + dependsOn(tasks.named('cleanDocs')) + dependsOn(upstream) + + access = GroovydocAccess.PROTECTED + includeAuthor = false + includeMainForScripts = true + processScripts = true + exclude('**/Application.groovy') + + + source = { upstream.get().source } + destinationDir = rootProject.layout.buildDirectory.dir('docs/gapi').get().asFile + classpath = files({ upstream.get().classpath }) + groovyClasspath = files({ upstream.get().groovyClasspath }) +} + +tasks.register('docs') { + description = 'Generates the documentation' + group = 'documentation' + + dependsOn( + 'aggregateGroovyApiDoc', + docProject.get().tasks.named('asciidoctor') + ) + finalizedBy( + 'copyAsciiDoctorDocs', + 'ghPagesRootIndexPage' + ) +} + +tasks.register('copyAsciiDoctorDocs', Copy) { + group = 'documentation' + + from(docProject.flatMap { it.layout.buildDirectory }) + into(rootProject.layout.buildDirectory) + include('docs/**') + includeEmptyDirs = false + +} + +tasks.register('ghPagesRootIndexPage') { + description = 'Provides a root index page with historical versions fetched from the gh-pages branch at build time' + group = 'documentation' + + def templateFile = docProject.map { it.layout.projectDirectory.file('src/docs/index.tmpl') } + def outputFile = rootProject.layout.buildDirectory.file('docs/ghpages.html') + + inputs.file(templateFile) + outputs.file(outputFile) + + doLast { + def githubUser = rootProject.findProperty('githubUser') as String + def githubProject = rootProject.findProperty('githubProject') as String + + List versions = [] + try { + def conn = URI.create("https://api.github.com/repos/${githubUser}/${githubProject}/contents/?ref=gh-pages").toURL().openConnection() + conn.setRequestProperty('Accept', 'application/vnd.github+json') + conn.setRequestProperty('User-Agent', 'gradle-docs-build') + def parsed = new groovy.json.JsonSlurper().parse(conn.inputStream) + versions = (parsed as List) + .findAll { it.type == 'dir' } + .collect { it.name as String } + .findAll { it ==~ /\d+\.\d+\.(\d+|x)(-.*)?/ } + .sort() + .reverse() + } catch (Exception e) { + logger.warn("ghPagesRootIndexPage: could not fetch GitHub versions — ${e.message}") + } + + String optionsHtml = versions + ? versions.collect { v -> "" }.join('\n ') + : '' + + def tokens = [ + '@OTHER_VERSIONS_OPTIONS@': optionsHtml, + '@GITHUB_REPO_URL@' : "https://github.com/${githubUser}/${githubProject}", + '@GITHUB_ORG_URL@' : "https://github.com/${githubUser}", + '@REPO_SLUG@' : githubProject, + ] + + def out = outputFile.get().asFile + out.parentFile.mkdirs() + def content = templateFile.get().asFile.text + tokens.each { token, value -> content = content.replace(token, value) } + out.text = content + } +} diff --git a/build-logic/src/main/groovy/config.example-app.gradle b/build-logic/src/main/groovy/config.example-app.gradle new file mode 100644 index 0000000..9d5446e --- /dev/null +++ b/build-logic/src/main/groovy/config.example-app.gradle @@ -0,0 +1,8 @@ +plugins { + id 'config.app-run' + id 'config.compile' + id 'config.grails-assets' + id 'config.testing' + id 'org.apache.grails.gradle.grails-web' + id 'org.apache.grails.gradle.grails-gsp' +} diff --git a/build-logic/src/main/groovy/config.grails-assets.gradle b/build-logic/src/main/groovy/config.grails-assets.gradle new file mode 100644 index 0000000..57b9381 --- /dev/null +++ b/build-logic/src/main/groovy/config.grails-assets.gradle @@ -0,0 +1,26 @@ +import asset.pipeline.gradle.AssetPipelineExtension + +plugins { + id 'cloud.wondrify.asset-pipeline' +} + +dependencies { + add('assetDevelopmentRuntime', 'org.webjars.npm:bootstrap') + add('assetDevelopmentRuntime', 'org.webjars.npm:bootstrap-icons') + add('assetDevelopmentRuntime', 'org.webjars.npm:jquery') +} + +extensions.configure(AssetPipelineExtension) { + it.excludes = [ + 'webjars/jquery/**', + 'webjars/bootstrap/**', + 'webjars/bootstrap-icons/**' + ] + it.includes = [ + 'webjars/jquery/*/dist/jquery.js', + 'webjars/bootstrap/*/dist/js/bootstrap.bundle.js', + 'webjars/bootstrap/*/dist/css/bootstrap.css', + 'webjars/bootstrap-icons/*/font/bootstrap-icons.css', + 'webjars/bootstrap-icons/*/font/fonts/*', + ] +} diff --git a/build-logic/src/main/groovy/config.grails-plugin.gradle b/build-logic/src/main/groovy/config.grails-plugin.gradle new file mode 100644 index 0000000..26ddbf4 --- /dev/null +++ b/build-logic/src/main/groovy/config.grails-plugin.gradle @@ -0,0 +1,16 @@ +import asset.pipeline.gradle.AssetPipelineExtension +import org.grails.gradle.plugin.core.GrailsExtension + +plugins { + id 'org.apache.grails.gradle.grails-plugin' + id 'cloud.wondrify.asset-pipeline' +} + +extensions.configure(GrailsExtension) { + // Plugins should avoid the spring dependency management plugin due to how it prefers certain libraries + it.springDependencyManagement = false +} + +extensions.configure(AssetPipelineExtension) { + it.packagePlugin = true +} diff --git a/build-logic/src/main/groovy/config.publish-root.gradle b/build-logic/src/main/groovy/config.publish-root.gradle new file mode 100644 index 0000000..49be5d9 --- /dev/null +++ b/build-logic/src/main/groovy/config.publish-root.gradle @@ -0,0 +1,16 @@ +// Workaround needed for nexus publishing bug +// version and group must be specified in the root project +// https://github.com/gradle-nexus/publish-plugin/issues/310 +version = projectVersion +group = 'io.github.gpc' + +def publishedProjects = projectsToPublish + .tokenize(',') + .collect { it.trim() } + .findAll() + +subprojects { + if (name in publishedProjects) { + apply(plugin: 'org.apache.grails.gradle.grails-publish') + } +} diff --git a/build-logic/src/main/groovy/config.publish.gradle b/build-logic/src/main/groovy/config.publish.gradle new file mode 100644 index 0000000..cf0e078 --- /dev/null +++ b/build-logic/src/main/groovy/config.publish.gradle @@ -0,0 +1,9 @@ +// Useful when testing a release version locally and not wanting to setup signing +pluginManager.withPlugin('signing') { + if (System.getenv('DISABLE_BUILD_SIGNING')) { + logger.lifecycle('Signing is disabled for this build per configuration.') + tasks.withType(Sign).configureEach { + enabled = false + } + } +} diff --git a/build-logic/src/main/groovy/config.testing.gradle b/build-logic/src/main/groovy/config.testing.gradle new file mode 100644 index 0000000..667d35b --- /dev/null +++ b/build-logic/src/main/groovy/config.testing.gradle @@ -0,0 +1,45 @@ +import com.adarshr.gradle.testlogger.TestLoggerExtension + +plugins { + id 'com.adarshr.test-logger' +} + +def isCi = System.getenv('CI') != null +def isWindows = System.getProperty('os.name')?.toLowerCase()?.contains('windows') + +// This configures the 'pretty' test logging +// mocha-parallel uses Unicode symbols that require special config on Windows; +// standard-parallel is a safe fallback there. +extensions.configure(TestLoggerExtension) { + it.theme = isCi ? 'plain-parallel' : (isWindows ? 'standard-parallel' : 'mocha-parallel') + it.showExceptions = true + it.showStandardStreams = false + it.showSummary = true + it.showPassed = true + it.showSkipped = true + it.showFailed = true +} + +tasks.withType(Test).configureEach { + onlyIf { + !project.hasProperty('skipTests') + } + + useJUnitPlatform() + + maxHeapSize = '1g' // set to match the groovy compile task to ensure the worker daemons are reused + + reports { + junitXml.required = false + html.required = true + } + + testLogging { + showStandardStreams = false + exceptionFormat = 'full' + stackTraceFilters = ['groovy'] + events = ['failed', 'skipped', 'standardError'] + showStackTraces = true + showCauses = true + } +} diff --git a/build.gradle b/build.gradle index 402930d..f4e064b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,188 +1,7 @@ -buildscript { - repositories { - mavenLocal() - maven { url "https://repo.grails.org/grails/core" } - } - dependencies { - classpath "org.grails:grails-gradle-plugin:$grailsGradlePluginVersion" - classpath "org.grails.plugins:hibernate5:$gormHibernate5Version" - classpath "io.github.gradle-nexus:publish-plugin:1.0.0" - } +plugins { + id 'idea' + id 'config.docs' + id 'config.publish-root' } -group "io.github.gpc" - -apply plugin:"org.grails.grails-plugin" -apply plugin: "maven-publish" -apply plugin: "signing" -apply plugin: "io.github.gradle-nexus.publish-plugin" - -repositories { - mavenLocal() - maven { url "https://repo.grails.org/grails/core" } -} - -dependencies { - implementation "org.grails:grails-core" - - implementation "org.springframework.boot:spring-boot-starter-logging" - implementation "org.springframework.boot:spring-boot-starter-actuator" - implementation "org.springframework.boot:spring-boot-starter-validation" - implementation "org.springframework.boot:spring-boot-autoconfigure" - implementation "org.springframework.boot:spring-boot-starter-tomcat" - - implementation "org.grails:grails-web-boot" - implementation "org.grails.plugins:hibernate5" - implementation "org.hibernate:hibernate-core" - implementation "org.grails:grails-logging" - implementation "org.grails:grails-plugin-rest" - implementation "org.grails:grails-plugin-databinding" - implementation "org.grails:grails-plugin-i18n" - implementation "org.grails:grails-plugin-services" - implementation "org.grails:grails-plugin-url-mappings" - implementation "org.grails:grails-plugin-interceptors" - implementation "org.grails:grails-datastore-gorm-hibernate5:$gormHibernate5Version" - implementation "org.grails.plugins:cache" - implementation "org.grails.plugins:async" - profile "org.grails.profiles:web-plugin" - runtimeOnly "com.h2database:h2" - runtimeOnly "org.apache.tomcat:tomcat-jdbc" - compileOnly "io.micronaut:micronaut-inject-groovy" - testImplementation "io.micronaut:micronaut-inject-groovy" - testImplementation "org.grails:grails-gorm-testing-support" - testImplementation "org.grails:grails-web-testing-support" - - console "org.grails:grails-console" -} - -jar { - exclude "grails/plugins/taggable/Test*" -} - -publishing { - publications { - maven(MavenPublication) { - groupId = project.group - artifactId = 'grails-taggable-plugin' - version = project.version - - from components.java - artifact sourcesJar - artifact javadocJar - - pom { - name = 'Taggable Grails Plugin' - description = 'The Taggable that adds a generic mechanism for tagging data.' - url = 'https://github.com/gpc/taggable' - licenses { - license { - name = 'The Apache License, Version 2.0' - url = 'https://www.apache.org/licenses/LICENSE-2.0.txt' - } - } - developers { - developer { - id = 'graemerocher' - name = 'Graeme Rocher' - } - developer { - id = 'marcpalmer' - name = 'Marc Palmer' - } - developer { - id = 'pledbrook' - name = 'Peter Ledbrook' - } - developer { - id = 'rhyolight' - name = 'Matthew Taylor ' - } - developer { - id = 'lhotari' - name = 'Lari Hotari' - } - developer { - id = 'codeconsole' - name = 'Scott Murphy' - } - developer { - id = 'jjelliott' - name = 'JJ' - } - developer { - id = 'cazacugmihai' - name = 'Mihai Cazacu' - } - developer { - id = 'stokito' - name = 'Sergey Ponomarev' - } - developer { - id = 'jeffbrown' - name = 'Jeff Scott Brown' - } - developer { - id = 'sbglasius' - name = 'Søren Berg Glasius' - } - } - scm { - connection = 'scm:git:git://github.com/gpc/taggable.git' - developerConnection = 'scm:git:ssh://github.com:gpc/taggable.git' - url = 'https://github.com/gpc/taggable' - } - } - } - } -} - -ext."signing.keyId" = project.findProperty('signing.keyId') ?: System.getenv('SIGNING_KEY_ID') -ext."signing.password" = project.findProperty('signing.password') ?: System.getenv('SIGNING_PASSPHRASE') -ext."signing.secretKeyRingFile" = project.findProperty('signing.secretKeyRingFile') ?: (System.getenv('SIGNING_PASSPHRASE') ?: "${System.getProperty('user.home')}/.gnupg/secring.gpg") - -ext.isReleaseVersion = !version.endsWith("SNAPSHOT") - -afterEvaluate { - signing { - required { isReleaseVersion } - sign publishing.publications.maven - } -} - -tasks.withType(Sign) { - onlyIf { isReleaseVersion } -} - -nexusPublishing { - repositories { - sonatype { - def ossUser = System.getenv("SONATYPE_USERNAME") ?: project.findProperty('sonatypeOss2Username') ?: '' - def ossPass = System.getenv("SONATYPE_PASSWORD") ?: project.findProperty("sonatypeOss2Password") ?: '' - def ossStagingProfileId = System.getenv("SONATYPE_STAGING_PROFILE_ID") ?: project.findProperty("sonatypeOssStagingProfileIdJms") ?: '' - - nexusUrl = uri("https://s01.oss.sonatype.org/service/local/") - snapshotRepositoryUrl = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/") - username = ossUser - password = ossPass - stagingProfileId = ossStagingProfileId - } - } -} - -task snapshotVersion { - doLast { - if (!project.version.endsWith('-SNAPSHOT')) { - ant.propertyfile(file: "gradle.properties") { - entry(key: "version", value: "${project.version}-SNAPSHOT") - } - } - } -} - -tasks.withType(Test) { - useJUnitPlatform() - testLogging { - events = ["passed", "failed", "skipped"] - showStandardStreams = false - } -} +// Intentionally left blank - use composition instead diff --git a/docs/build.gradle b/docs/build.gradle new file mode 100644 index 0000000..efdbab7 --- /dev/null +++ b/docs/build.gradle @@ -0,0 +1,41 @@ +import org.asciidoctor.gradle.jvm.AsciidoctorTask + +plugins { + id 'org.asciidoctor.jvm.convert' +} + +version = projectVersion +group = 'io.github.gpc' + +def asciidoctorAttributes = [ + 'source-highlighter': 'coderay', + toc : 'left', + toclevels : '2', + 'toc-title' : 'Table of Contents', + icons : 'font', + id : "${project.name - '-docs'}:${project.version}", + idprefix : '', + idseparator : '-', + version : project.version, + projectUrl : "https://github.com/${rootProject.findProperty('githubUser')}/${rootProject.findProperty('githubProject')}", + sourcedir : "${rootProject.allprojects.find { it.name == 'taggable' }.projectDir}/src/main/groovy", + grailsVersion : grailsVersion +] + +tasks.named('asciidoctor', AsciidoctorTask) { + + outputDir = project.layout.buildDirectory.dir('docs') + sourceDir = project.layout.projectDirectory.dir('src/docs') + + attributes(asciidoctorAttributes) + baseDirFollowsSourceDir() + options(doctype: 'book') + sources { include 'index.adoc' } + + jvm { + jvmArgs( + '--add-opens', 'java.base/java.io=ALL-UNNAMED', + '--add-opens', 'java.base/sun.nio.ch=ALL-UNNAMED' + ) + } +} diff --git a/docs/src/docs/gettingStarted.adoc b/docs/src/docs/gettingStarted.adoc new file mode 100644 index 0000000..a31a642 --- /dev/null +++ b/docs/src/docs/gettingStarted.adoc @@ -0,0 +1,52 @@ +[[gettingStarted]] +== Getting Started + +=== Requirements + +* Grails {grailsVersion} +* Java 17+ + +=== Installation + +Add the plugin to your application's `build.gradle`: + +[source,groovy,subs="attributes"] +---- +dependencies { + implementation 'io.github.gpc:taggable:{version}' +} +---- + +=== Case sensitivity + +By default, all tags are forced to lower case. Set the following in `application.yml` to preserve the supplied case: + +[source,yaml] +---- +grails: + taggable: + preserve: + case: true +---- + +Finder methods continue to treat each tag as a discrete string, so `"grails"` and `"Grails"` are independent tags when +case preservation is on. + +=== Custom table names + +[source,groovy] +---- +grails.taggable.tag.table = 'MY_TAGS' +grails.taggable.tagLink.table = 'MY_TAG_LINKS' +---- + +=== Hibernate autoImport + +Auto-import is disabled by default for `Tag` / `TagLink` to avoid conflicts with consumer-domain classes of the same +name. Re-enable if required: + +[source,groovy] +---- +grails.taggable.tag.autoImport = true +grails.taggable.tagLink.autoImport = true +---- diff --git a/docs/src/docs/index.adoc b/docs/src/docs/index.adoc new file mode 100644 index 0000000..60f8e2a --- /dev/null +++ b/docs/src/docs/index.adoc @@ -0,0 +1,8 @@ += Taggable Plugin - Reference Documentation +Grails Plugin Collective + +include::introduction.adoc[] + +include::gettingStarted.adoc[] + +include::usage.adoc[] diff --git a/docs/src/docs/index.tmpl b/docs/src/docs/index.tmpl new file mode 100644 index 0000000..a13d6a0 --- /dev/null +++ b/docs/src/docs/index.tmpl @@ -0,0 +1,389 @@ + + + + + + Export - Grails Plugin + + + + + + +
+ + + + + View on GitHub + +
+

Export

+

A Grails plugin that adds export functionality for CSV, Excel, ODS, PDF, RTF, and XML formats

+
+
+ +
+

Documentation

+ +
+
+ Latest Release +

Stable Version

+ +
+ +
+ Snapshot +

Development Version

+ +
+
+ +

Other Versions

+
+
+ + +
+
+
+ + + + + + diff --git a/docs/src/docs/introduction.adoc b/docs/src/docs/introduction.adoc new file mode 100644 index 0000000..99090bb --- /dev/null +++ b/docs/src/docs/introduction.adoc @@ -0,0 +1,12 @@ +[[introduction]] +== Introduction + +The Taggable plugin adds a generic mechanism for tagging any Grails domain class. It supports method chaining, +polymorphic tag queries across class hierarchies, configurable case sensitivity, and a GSP tag library for rendering +tag clouds. + +include::introduction/sourceCode.adoc[] + +include::introduction/currentVersion.adoc[] + +include::introduction/license.adoc[] diff --git a/docs/src/docs/introduction/currentVersion.adoc b/docs/src/docs/introduction/currentVersion.adoc new file mode 100644 index 0000000..a35dc9d --- /dev/null +++ b/docs/src/docs/introduction/currentVersion.adoc @@ -0,0 +1,14 @@ +[[currentVersion]] +=== Current version + +Current version is *{revnumber}*. + +|=== +| Plugin release | Supported Grails Version +| 7.x | >= 7.0 +| 6.x | 6.x +| 5.x | 5.x +| 4.x | 4.x +|=== + +This version is developed against *Grails {grailsVersion}*. diff --git a/docs/src/docs/introduction/license.adoc b/docs/src/docs/introduction/license.adoc new file mode 100644 index 0000000..2d2a9af --- /dev/null +++ b/docs/src/docs/introduction/license.adoc @@ -0,0 +1,6 @@ +[[license]] +=== License + +This plugin is released under the http://www.apache.org/licenses/LICENSE-2.0[Apache License, Version 2.0]. + +See https://github.com/gpc/taggable/blob/master/LICENSE.txt for the full licence text. diff --git a/docs/src/docs/introduction/sourceCode.adoc b/docs/src/docs/introduction/sourceCode.adoc new file mode 100644 index 0000000..a7529ac --- /dev/null +++ b/docs/src/docs/introduction/sourceCode.adoc @@ -0,0 +1,12 @@ +[[sourceCode]] +=== Source code + +The full source code for this plugin can be found on https://github.com/gpc/grails-export[GitHub]. + +The master branch is for the Grails 3.x version. The code for the Grails 2.x version is on branch: `grails-2.x`. + +For issues, improvements or new features go to the plugin's go to https://github.com/gpc/grails-export/issues[GitHub issues]. + +To contact me directly my email address is puneet DOT behl007 AT gmail DOT com + +Feel free to send me any correction about this document. \ No newline at end of file diff --git a/docs/src/docs/usage.adoc b/docs/src/docs/usage.adoc new file mode 100644 index 0000000..48451e3 --- /dev/null +++ b/docs/src/docs/usage.adoc @@ -0,0 +1,93 @@ +[[usage]] +== Usage + +=== Making a domain class taggable + +[source,groovy] +---- +import grails.plugins.taggable.* + +class Vehicle implements Taggable { } +---- + +=== Adding, removing, and replacing tags + +[source,groovy] +---- +def v = Vehicle.get(1) + +v.addTag('red') + .addTag('sporty') + .addTag('expensive') + +v.addTags(['electric', 'hybrid']) + +v.setTags(['red', 'sporty', 'expensive']) + +v.removeTag('expensive') + +def tags = v.tags // ['red', 'sporty'] +---- + +=== Querying by tag + +[source,groovy] +---- +def vehicles = Vehicle.findAllByTag('sporty') // also accepts [max: 5] etc. +def count = Vehicle.countByTag('sporty') +def totalTags = Vehicle.totalTags +def allTags = Vehicle.allTags + +def teslaElectricCars = Vehicle.findAllByTagWithCriteria('electric') { + eq('manufacturer', 'Tesla Motors') +} + +def fiveCoolTags = Vehicle.findAllTagsWithCriteria([max: 5]) { + ilike('name', '%cool%') +} +---- + +=== Querying by tag with HQL + +`Tag` and `TagLink` are mapped GORM entities; you can use them in HQL: + +[source,groovy] +---- +def gasGuzzlers = Vehicle.executeQuery(''' + SELECT vehicle + FROM Vehicle vehicle, TagLink tagLink + WHERE vehicle.id = tagLink.tagRef + AND tagLink.type = 'Vehicle' + AND tagLink.tag.name IN (:tags) +''', [tags: ['SUV', 'gas-guzzler', 'tank', 'boat']]) +---- + +=== Parsing delimited tag strings + +[source,groovy] +---- +v.parseTags('red,sporty,expensive') +v.parseTags('red/sporty/expensive', '/') +---- + +=== Tag cloud taglib + +[source,html] +---- + +---- + +Override the default CSS size classes via configuration: + +[source,yaml] +---- +grails: + taggable: + css: + classes: + - tiny + - small + - medium + - large + - huge +---- diff --git a/examples/app1/build.gradle b/examples/app1/build.gradle new file mode 100644 index 0000000..3e6a918 --- /dev/null +++ b/examples/app1/build.gradle @@ -0,0 +1,47 @@ +plugins { + id 'config.example-app' +} + +version = projectVersion +group = 'app1' + +dependencies { + implementation project(':taggable') + profile "org.apache.grails.profiles:web" + developmentOnly "org.springframework.boot:spring-boot-devtools" + // Spring Boot DevTools may cause performance slowdowns or compatibility issues on larger applications + testAndDevelopmentOnly "org.webjars.npm:bootstrap" + testAndDevelopmentOnly "org.webjars.npm:bootstrap-icons" + testAndDevelopmentOnly "org.webjars.npm:jquery" + implementation platform("org.apache.grails:grails-bom:$grailsVersion") + implementation "org.apache.grails:grails-core" + implementation "org.apache.grails:grails-data-hibernate5" + implementation "org.apache.grails:grails-databinding" + implementation "org.apache.grails:grails-events" + implementation "org.apache.grails:grails-gsp" + implementation "org.apache.grails:grails-interceptors" + implementation "org.apache.grails:grails-layout" + implementation "org.apache.grails:grails-logging" + implementation "org.apache.grails:grails-rest-transforms" + implementation "org.apache.grails:grails-scaffolding" + implementation "org.apache.grails:grails-services" + implementation "org.apache.grails:grails-url-mappings" + implementation "org.apache.grails:grails-web-boot" + implementation "org.springframework.boot:spring-boot-autoconfigure" + implementation "org.springframework.boot:spring-boot-starter" + implementation "org.springframework.boot:spring-boot-starter-actuator" + implementation "org.springframework.boot:spring-boot-starter-logging" + implementation "org.springframework.boot:spring-boot-starter-tomcat" + implementation "org.springframework.boot:spring-boot-starter-validation" + console "org.apache.grails:grails-console" + runtimeOnly "cloud.wondrify:asset-pipeline-grails" + runtimeOnly "com.h2database:h2" + runtimeOnly "com.zaxxer:HikariCP" + runtimeOnly "org.fusesource.jansi:jansi" + integrationTestImplementation testFixtures("org.apache.grails:grails-geb") + testImplementation "org.apache.grails:grails-testing-support-datamapping" + testImplementation "org.apache.grails:grails-testing-support-web" + testImplementation "org.mockito:mockito-core" + testImplementation "org.spockframework:spock-core" +} + diff --git a/examples/app1/grails-app/assets/stylesheets/application.css b/examples/app1/grails-app/assets/stylesheets/application.css new file mode 100644 index 0000000..c1b6dbb --- /dev/null +++ b/examples/app1/grails-app/assets/stylesheets/application.css @@ -0,0 +1,5 @@ +/* + * This is a manifest file that'll be compiled into application.css. + * + *= require_self + */ diff --git a/examples/app1/grails-app/conf/application.yml b/examples/app1/grails-app/conf/application.yml new file mode 100644 index 0000000..86ab46f --- /dev/null +++ b/examples/app1/grails-app/conf/application.yml @@ -0,0 +1,93 @@ +--- +grails: + profile: web + codegen: + defaultPackage: grails.plugins.taggable +info: + app: + name: '@info.app.name@' + version: '@info.app.version@' + grailsVersion: '@info.app.grailsVersion@' +spring: + groovy: + template: + check-template-location: false +server: + port: 8081 + +--- +grails: + mime: + disable: + accept: + header: + userAgents: + - Gecko + - WebKit + - Presto + - Trident + types: + all: '*/*' + atom: application/atom+xml + css: text/css + csv: text/csv + form: application/x-www-form-urlencoded + html: + - text/html + - application/xhtml+xml + js: text/javascript + json: + - application/json + - text/json + multipartForm: multipart/form-data + rss: application/rss+xml + text: text/plain + hal: + - application/hal+json + - application/hal+xml + xml: + - text/xml + - application/xml + urlmapping: + cache: + maxsize: 1000 + controllers: + defaultScope: singleton + converters: + encoding: UTF-8 + views: + default: + codec: html + gsp: + encoding: UTF-8 + htmlcodec: xml + codecs: + expression: html + scriptlets: html + taglib: none + staticparts: none +--- +dataSource: + driverClassName: org.h2.Driver + username: sa + password: '' + pooled: true + jmxExport: true +environments: + development: + dataSource: + dbCreate: create-drop + url: jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE + test: + dataSource: + dbCreate: create-drop + url: jdbc:h2:mem:testDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE + production: + dataSource: + dbCreate: none + url: jdbc:h2:./prodDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE +hibernate: + cache: + queries: false + use_second_level_cache: false + use_query_cache: false diff --git a/grails-app/conf/logback.xml b/examples/app1/grails-app/conf/logback.xml similarity index 100% rename from grails-app/conf/logback.xml rename to examples/app1/grails-app/conf/logback.xml diff --git a/grails-app/domain/grails/plugins/taggable/TestDescendent.groovy b/examples/app1/grails-app/domain/grails/plugins/taggable/TestDescendent.groovy similarity index 100% rename from grails-app/domain/grails/plugins/taggable/TestDescendent.groovy rename to examples/app1/grails-app/domain/grails/plugins/taggable/TestDescendent.groovy diff --git a/grails-app/domain/grails/plugins/taggable/TestDomain.groovy b/examples/app1/grails-app/domain/grails/plugins/taggable/TestDomain.groovy similarity index 100% rename from grails-app/domain/grails/plugins/taggable/TestDomain.groovy rename to examples/app1/grails-app/domain/grails/plugins/taggable/TestDomain.groovy diff --git a/grails-app/init/grails/plugins/taggable/Application.groovy b/examples/app1/grails-app/init/grails/plugins/taggable/Application.groovy similarity index 80% rename from grails-app/init/grails/plugins/taggable/Application.groovy rename to examples/app1/grails-app/init/grails/plugins/taggable/Application.groovy index 09408b0..435540a 100644 --- a/grails-app/init/grails/plugins/taggable/Application.groovy +++ b/examples/app1/grails-app/init/grails/plugins/taggable/Application.groovy @@ -1,13 +1,10 @@ package grails.plugins.taggable - import grails.boot.GrailsApp import grails.boot.config.GrailsAutoConfiguration -import grails.plugins.metadata.PluginSource -@PluginSource class Application extends GrailsAutoConfiguration { static void main(String[] args) { GrailsApp.run(Application, args) } -} \ No newline at end of file +} diff --git a/examples/app1/grails-app/views/layouts/main.gsp b/examples/app1/grails-app/views/layouts/main.gsp new file mode 100644 index 0000000..137125a --- /dev/null +++ b/examples/app1/grails-app/views/layouts/main.gsp @@ -0,0 +1,13 @@ + + + + + + <g:layoutTitle default="Taggable"/> + + + + + + + diff --git a/examples/app1/src/integration-test/groovy/grails/plugins/taggable/TaggableSpec.groovy b/examples/app1/src/integration-test/groovy/grails/plugins/taggable/TaggableSpec.groovy new file mode 100644 index 0000000..4fa065a --- /dev/null +++ b/examples/app1/src/integration-test/groovy/grails/plugins/taggable/TaggableSpec.groovy @@ -0,0 +1,283 @@ +package grails.plugins.taggable + +import spock.lang.Specification + +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + +@Integration +@Rollback +class TaggableSpec extends Specification { + + void setup() { + Tag.preserveCaseForTesting = false + } + + void testAddTagMethodCaseInsensitive() { + given: + def td = new TestDomain(name: "foo") + td.save() + + when: + td.addTag("Groovy") + .addTag("grails") + and: + def links = TagLink.findAllWhere(tagRef: td.id, type: 'testDomain') + + then: + links.size() == 2 + links.tag.name == ['groovy', 'grails'] + } + + void testAddTagMethodCasePreserving() { + given: + Tag.preserveCaseForTesting = true + + def td = new TestDomain(name: "foo") + td.save() + + when: + td.addTag("Groovy") + .addTag("grails") + + def links = TagLink.findAllWhere(tagRef: td.id, type: 'testDomain') + then: + links.size() == 2 + links.tag.name == ['Groovy', 'grails'] + + when: + // adding a second, even if preserving case in DB it should still not add it as already has such a tag + td.addTag("groovy") + and: + links = TagLink.findAllWhere(tagRef: td.id, type: 'testDomain') + + then: + links.size() == 3 + links.tag.name == ['Groovy', 'grails', 'groovy'] + } + + void testAddTagsMethod() { + given: + def td = new TestDomain(name: "foo") + td.save() + + when: + td.addTags(["groovy", "grails"]) + and: + def links = TagLink.findAllWhere(tagRef: td.id, type: 'testDomain') + + then: + links.size() == 2 + links.tag.name == ['groovy', 'grails'] + } + + void testRemoveTagMethod() { + given: + def td = new TestDomain(name: "foo") + td.save() + + when: + td.addTag("groovy") + .addTag("grails") + and: + def links = TagLink.findAllWhere(tagRef: td.id, type: 'testDomain') + + then: + links.size() == 2 + links.tag.name == ['groovy', 'grails'] + + when: + td.removeTag("groovy") + and: + links = TagLink.findAllWhere(tagRef: td.id, type: 'testDomain') + + then: + links.size() == 1 + links.tag.name == ['grails'] + } + + void testGetTagsMethod() { + given: + def td = new TestDomain(name: "foo") + td.save() + + when: + td.addTag("groovy") + .addTag("grails") + td.save(flush: true) + + and: + TestDomain.withSession { session -> session.clear() } + td = TestDomain.findByName("foo") + + then: + td.tags == ['groovy', 'grails'] + } + + void testSetTagsMethod() { + given: + def td = new TestDomain(name: "foo") + td.save() + + when: + td.tags = ["groovy", null, "grails", ''] + and: + def links = TagLink.findAllWhere(tagRef: td.id, type: 'testDomain') + + then: + links.size() == 2 + links.tag.name == ['groovy', 'grails'] + td.tags == ['groovy', 'grails'] + + when: + td.tags = ["foo", "bar"] + and: + links = TagLink.findAllWhere(tagRef: td.id, type: 'testDomain') + + then: + links.size() == 2 + links.tag.name.sort(true) == ['foo', 'bar'].sort(true) + td.tags.sort(true) == ['foo', 'bar'].sort(true) + + when: + td.tags = [] + and: + links = TagLink.findAllWhere(tagRef: td.id, type: 'testDomain') + + then: + links.size() == 0 + links.tag.name == [] + td.tags == [] + } + + void testFindAllByTag() { + given: + new TestDomain(name: "foo") + .save() + .addTag("groovy") + .addTag("grails") + .addTag("griffon") + new TestDomain(name: "bar") + .save() + .addTag("groovy") + .addTag("grails") + + when: + def results = TestDomain.findAllByTag("groovy") + + then: + results.size() == 2 + results[0] instanceof TestDomain + TestDomain.findAllByTag("groovy").size() == 2 + TestDomain.findAllByTag("grails").size() == 2 + TestDomain.findAllByTag("griffon").size() == 1 + TestDomain.findAllByTag("nothing").size() == 0 + TestDomain.findAllByTag(null).size() == 0 + } + + void testFindAllByTagPolymorphic() { + given: + new TestDomain(name: "foo") + .save() + .addTag("groovy") + .addTag("grails") + .addTag("griffon") + new TestDescendent(name: "bar", other: 'bla') + .save() + .addTag("groovy") + .addTag("grails") + .addTag("gradle") + + when: + def results = TestDomain.findAllByTag("groovy") + + then: + results.size() == 2 + results[0] instanceof TestDomain + TestDomain.findAllByTag("groovy").size() == 2 + TestDescendent.findAllByTag("groovy").size() == 1 + TestDomain.findAllByTag("grails").size() == 2 + TestDescendent.findAllByTag("grails").size() == 1 + TestDomain.findAllByTag("gradle").size() == 1 + TestDescendent.findAllByTag("gradle").size() == 1 + TestDomain.findAllByTag("griffon").size() == 1 + TestDomain.findAllByTag("nothing").size() == 0 + TestDomain.findAllByTag(null).size() == 0 + } + + void testCountByTag() { + given: + new TestDomain(name: "foo") + .save() + .addTag("groovy") + .addTag("grails") + .addTag("griffon") + new TestDomain(name: "bar") + .save() + .addTag("groovy") + .addTag("grails") + new TestDescendent(name: "bla", other: 'zzzz') + .save() + .addTag("groovy") + .addTag("grails") + .addTag("gradle") + + expect: + TestDomain.countByTag("groovy") == 3 + TestDescendent.countByTag("groovy") == 1 + TestDomain.countByTag("griffon") == 1 + TestDescendent.countByTag("griffon") == 0 + TestDomain.countByTag("gradle") == 1 + TestDescendent.countByTag("gradle") == 1 + TestDomain.countByTag("rubbish") == 0 + TestDomain.countByTag(null) == 0 + } + + void testAllTags() { + given: + new TestDomain(name: "foo") + .save() + .addTag("groovy") + .addTag("grails") + .addTag("griffon") + new TestDomain(name: "bar") + .save() + .addTag("groovy") + .addTag("grails") + new TestDescendent(name: "bla", other: 'zzzz') + .save() + .addTag("groovy") + .addTag("grails") + .addTag("gradle") + + expect: + TestDomain.allTags.sort(true) == ['gradle', 'grails', 'griffon', 'groovy'].sort(true) + TestDomain.totalTags == 4 + TestDescendent.allTags.sort(true) == ['gradle', 'grails', 'groovy'].sort(true) + TestDescendent.totalTags == 3 + } + + void testParseTags() { + given: + def td = new TestDomain(name: "foo") + .save() + + when: + td.parseTags("groovy,grails,griffon") + + then: + TestDomain.allTags == ['grails', 'griffon', 'groovy'] + } + + void testParseTagsWithDelimiter() { + given: + def td = new TestDomain(name: "foo") + .save() + + when: + td.parseTags("groovy grails griffon", " ") + + then: + TestDomain.allTags == ['grails', 'griffon', 'groovy'] + } +} diff --git a/gradle.properties b/gradle.properties index 30982f7..371a5a2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,9 +1,18 @@ -version=6.0.0-SNAPSHOT -grailsVersion=6.1.0 -gorm.version=8.0.2 -gormHibernate5Version=8.0.1 -grailsGradlePluginVersion=6.1.0 -groovyVersion=3.0.11 +projectVersion=7.0.0-SNAPSHOT +grailsVersion=7.0.10 +githubUser=gpc +githubProject=taggable +# Comma-separated list of projects to publish +projectsToPublish=taggable +# Build dependencies +asciidoctorVersion=4.0.5 +testLoggerVersion=4.0.0 +# Enable and set agree=yes to publish build scans from GitHub workflows +ciBuildScanPublish=true +ciBuildScanTermsOfUseUrl=https://gradle.com/terms-of-service +ciBuildScanTermsOfUseAgree=yes + +org.gradle.caching=true org.gradle.daemon=true org.gradle.parallel=true -org.gradle.jvmargs=-Dfile.encoding=UTF-8 -Xmx1024M \ No newline at end of file +org.gradle.jvmargs=-Dfile.encoding=UTF-8 -Xmx1024M diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index deedc7fa5e6310eac3148a7dd0b1f069b07364cb..1b33c55baabb587c669f562ae36f953de2481846 100644 GIT binary patch literal 43764 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≺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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d2d5d93..aaaabb3 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip diff --git a/gradlew b/gradlew index 9d82f78..23d15a9 100755 --- a/gradlew +++ b/gradlew @@ -1,74 +1,129 @@ -#!/usr/bin/env bash +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum -warn ( ) { +warn () { echo "$*" -} +} >&2 -die ( ) { +die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null +CLASSPATH="\\\"\\\"" -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -77,84 +132,120 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index aec9973..5eed7ee 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,4 +1,22 @@ -@if "%DEBUG%" == "" @echo off +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -8,26 +26,30 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -35,54 +57,36 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/grails-app/conf/application.yml b/grails-app/conf/application.yml deleted file mode 100644 index 61b5bf3..0000000 --- a/grails-app/conf/application.yml +++ /dev/null @@ -1,110 +0,0 @@ ---- -grails: - profile: web-plugin - codegen: - defaultPackage: grails.plugins.taggable -info: - app: - name: '@info.app.name@' - version: '@info.app.version@' - grailsVersion: '@info.app.grailsVersion@' -spring: - groovy: - template: - check-template-location: false - ---- -grails: - mime: - disable: - accept: - header: - userAgents: - - Gecko - - WebKit - - Presto - - Trident - types: - all: '*/*' - atom: application/atom+xml - css: text/css - csv: text/csv - form: application/x-www-form-urlencoded - html: - - text/html - - application/xhtml+xml - js: text/javascript - json: - - application/json - - text/json - multipartForm: multipart/form-data - rss: application/rss+xml - text: text/plain - hal: - - application/hal+json - - application/hal+xml - xml: - - text/xml - - application/xml - urlmapping: - cache: - maxsize: 1000 - controllers: - defaultScope: singleton - converters: - encoding: UTF-8 - views: - default: - codec: html - gsp: - encoding: UTF-8 - htmlcodec: xml - codecs: - expression: html - scriptlets: html - taglib: none - staticparts: none ---- -hibernate: - cache: - queries: false - use_second_level_cache: false - use_query_cache: false -dataSource: - pooled: true - jmxExport: true - driverClassName: org.h2.Driver - username: sa - password: - -environments: - development: - dataSource: - dbCreate: create-drop - url: jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE - test: - dataSource: - dbCreate: update - url: jdbc:h2:mem:testDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE - production: - dataSource: - dbCreate: update - url: jdbc:h2:prodDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE - properties: - jmxEnabled: true - initialSize: 5 - maxActive: 50 - minIdle: 5 - maxIdle: 25 - maxWait: 10000 - maxAge: 600000 - timeBetweenEvictionRunsMillis: 5000 - minEvictableIdleTimeMillis: 60000 - validationQuery: SELECT 1 - validationQueryTimeout: 3 - validationInterval: 15000 - testOnBorrow: true - testWhileIdle: true - testOnReturn: false - jdbcInterceptors: ConnectionState - defaultTransactionIsolation: 2 # TRANSACTION_READ_COMMITTED diff --git a/plugin/build.gradle b/plugin/build.gradle new file mode 100644 index 0000000..e8cdee9 --- /dev/null +++ b/plugin/build.gradle @@ -0,0 +1,51 @@ +plugins { + id 'config.compile' + id 'config.grails-plugin' + id 'config.publish' + id 'config.testing' +} + +version = projectVersion +group = 'io.github.gpc' + +dependencies { + profile 'org.apache.grails.profiles:web-plugin' + console 'org.apache.grails:grails-console' + + compileOnly platform("org.apache.grails:grails-bom:$grailsVersion") + compileOnly 'org.apache.grails:grails-dependencies-starter-web' + compileOnly 'org.apache.grails.data:grails-datastore-core' + + testImplementation platform("org.apache.grails:grails-bom:$grailsVersion") + testImplementation 'org.apache.grails:grails-dependencies-starter-web' + testImplementation 'org.apache.grails:grails-dependencies-test' + testImplementation 'org.apache.grails:grails-testing-support-web' +} + +extensions.configure(org.apache.grails.gradle.publish.GrailsPublishExtension) { + def githubUser = rootProject.findProperty('githubUser') + def githubProject = rootProject.findProperty('githubProject') + it.artifactId = project.name + it.githubSlug = "${githubUser}/${githubProject}" + it.license.name = 'Apache-2.0' + it.title = 'Grails Taggable Plugin' + it.desc = 'Adds a generic mechanism for tagging any Grails domain class, with tag-cloud taglib support.' + it.organization { + it.name = 'Grails Plugin Collective (GPC)' + it.url = "https://github.com/${githubUser}" + } + it.developers = [ + graemerocher : 'Graeme Rocher', + pledbrook : 'Peter Ledbrook', + marcpalmer : 'Marc Palmer', + jjelliott : 'jjelliott', + codeconsole : 'Scott Murphy', + sbglasius : 'Søren Berg Glasius', + lhotari : 'Lari Hotari', + rhyolight : 'Matthew Taylor', + eusorov : 'Evgeny Usorov', + cazacugmihai : 'Mihai Cazacu', + jeffscottbrown: 'Jeff Scott Brown', + stokito : 'Sergey Ponomarev', + ] +} diff --git a/plugin/grails-app/assets/images/.gitkeep b/plugin/grails-app/assets/images/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/plugin/grails-app/conf/application.yml b/plugin/grails-app/conf/application.yml new file mode 100644 index 0000000..f9adc84 --- /dev/null +++ b/plugin/grails-app/conf/application.yml @@ -0,0 +1,10 @@ +--- +grails: + profile: web-plugin + codegen: + defaultPackage: grails.plugins.taggable +info: + app: + name: '@info.app.name@' + version: '@info.app.version@' + grailsVersion: '@info.app.grailsVersion@' diff --git a/grails-app/domain/grails/plugins/taggable/Tag.groovy b/plugin/grails-app/domain/grails/plugins/taggable/Tag.groovy similarity index 100% rename from grails-app/domain/grails/plugins/taggable/Tag.groovy rename to plugin/grails-app/domain/grails/plugins/taggable/Tag.groovy diff --git a/grails-app/domain/grails/plugins/taggable/TagLink.groovy b/plugin/grails-app/domain/grails/plugins/taggable/TagLink.groovy similarity index 100% rename from grails-app/domain/grails/plugins/taggable/TagLink.groovy rename to plugin/grails-app/domain/grails/plugins/taggable/TagLink.groovy diff --git a/grails-app/services/grails/plugins/taggable/TaggableService.groovy b/plugin/grails-app/services/grails/plugins/taggable/TaggableService.groovy similarity index 51% rename from grails-app/services/grails/plugins/taggable/TaggableService.groovy rename to plugin/grails-app/services/grails/plugins/taggable/TaggableService.groovy index dbedd7a..52f024e 100644 --- a/grails-app/services/grails/plugins/taggable/TaggableService.groovy +++ b/plugin/grails-app/services/grails/plugins/taggable/TaggableService.groovy @@ -1,7 +1,6 @@ package grails.plugins.taggable import grails.core.GrailsApplication -import grails.core.GrailsDomainClass import grails.util.GrailsNameUtils import org.grails.datastore.mapping.model.MappingContext import org.grails.datastore.mapping.model.PersistentEntity @@ -13,7 +12,7 @@ class TaggableService { GrailsApplication grailsApplication @Autowired - @Qualifier("grailsDomainClassMappingContext") + @Qualifier('grailsDomainClassMappingContext') MappingContext mappingContext def domainClassFamilies = [:] @@ -33,30 +32,21 @@ class TaggableService { } return tagCounts } - + /** - * Update the graph of known subclasses - * - * Example: - * [ - * WcmContent: [ - * WcmBlog, - * WcmHTMLContent, - * WcmComment - * ] - * WcmBlog: [], - * WcmHTMLContent: [WcmRichContent], - * WcmRichContent: [], - * WcmStatus: [] - * ] + * Update the graph of known subclasses for each Taggable domain type. */ def refreshDomainClasses() { - grailsApplication.domainClasses.each {GrailsDomainClass artefact -> - PersistentEntity persistentEntity = mappingContext.getPersistentEntity(artefact.clazz.name) - if (Taggable.class.isAssignableFrom(artefact.clazz)) { - domainClassFamilies[artefact.clazz.name] = [GrailsNameUtils.getPropertyName(artefact.clazz)] - // Add class and all subclasses - domainClassFamilies[artefact.clazz.name].addAll(mappingContext.getChildEntities(persistentEntity).collect {GrailsNameUtils.getPropertyName(it.javaClass)}) + mappingContext.persistentEntities.each { PersistentEntity persistentEntity -> + Class clazz = persistentEntity.javaClass + if (Taggable.isAssignableFrom(clazz)) { + def family = [GrailsNameUtils.getPropertyName(clazz)] + family.addAll( + mappingContext.getChildEntities(persistentEntity).collect { + GrailsNameUtils.getPropertyName(it.javaClass) + } + ) + domainClassFamilies[clazz.name] = family } } } diff --git a/grails-app/taglib/grails/plugins/taggable/TagsTagLib.groovy b/plugin/grails-app/taglib/grails/plugins/taggable/TagsTagLib.groovy similarity index 100% rename from grails-app/taglib/grails/plugins/taggable/TagsTagLib.groovy rename to plugin/grails-app/taglib/grails/plugins/taggable/TagsTagLib.groovy diff --git a/src/main/groovy/grails/plugins/taggable/TagException.groovy b/plugin/src/main/groovy/grails/plugins/taggable/TagException.groovy similarity index 100% rename from src/main/groovy/grails/plugins/taggable/TagException.groovy rename to plugin/src/main/groovy/grails/plugins/taggable/TagException.groovy diff --git a/src/main/groovy/grails/plugins/taggable/Taggable.groovy b/plugin/src/main/groovy/grails/plugins/taggable/Taggable.groovy similarity index 54% rename from src/main/groovy/grails/plugins/taggable/Taggable.groovy rename to plugin/src/main/groovy/grails/plugins/taggable/Taggable.groovy index c4efd6b..6406369 100644 --- a/src/main/groovy/grails/plugins/taggable/Taggable.groovy +++ b/plugin/src/main/groovy/grails/plugins/taggable/Taggable.groovy @@ -14,22 +14,26 @@ */ package grails.plugins.taggable -import grails.util.* + +import grails.util.GrailsNameUtils +import grails.util.Holders +import org.grails.datastore.mapping.query.api.BuildableCriteria + /** * Marker interface to add tagging capabilities to a particular domain instance * @author Graeme Rocher */ trait Taggable { - Taggable addTag(String name) { - if(this.id == null) throw new TagException("You need to save the domain instance before tagging it") + Taggable addTag(String name) { + if (this.id == null) throw new TagException("You need to save the domain instance before tagging it") def tag if (!Tag.preserveCase) { name = name.toLowerCase() } - tag = Tag.findByName(name, [cache:true]) ?: new Tag(name:name).save() - if(!tag) throw new TagException("Value [$name] is not a valid tag") - + tag = Tag.findByName(name, [cache: true]) ?: new Tag(name: name).save() + if (!tag) throw new TagException("Value [$name] is not a valid tag") + def criteria = TagLink.createCriteria() def instance = this def link = criteria.get { @@ -38,44 +42,44 @@ trait Taggable { criteria.eq 'type', GrailsNameUtils.getPropertyName(instance.class) criteria.cache true } - - if(!link) { - link = new TagLink(tag:tag, tagRef:this.id, type:GrailsNameUtils.getPropertyName(this.class)).save() + + if (!link) { + link = new TagLink(tag: tag, tagRef: this.id, type: GrailsNameUtils.getPropertyName(this.class)).save() } return this // for method chaining - } + } - - Taggable addTags( names ) { + + Taggable addTags(names) { names.each { addTag it.toString() } return this } - + Collection tags() { - getTags() + getTags() } def getTags() { - this.id ? getTagLinks(Holders.applicationContext.taggableService, this).tag.name : [] - } + this.id ? getTagLinks(this).tag.name : [] + } - Taggable parseTags( String tags, String delimiter = "," ) { - tags.split(delimiter).each { + Taggable parseTags(String tags, String delimiter = ",") { + tags.split(delimiter).each { def tag = it.trim() - if(tag) addTag(tag) + if (tag) addTag(tag) } return this } - Taggable removeTag( String name ) { - if(this.id == null) throw new TagException("You need to save the domain instance before tagging it") - + Taggable removeTag(String name) { + if (this.id == null) throw new TagException("You need to save the domain instance before tagging it") + if (!Tag.preserveCase) { name = name.toLowerCase() } - + def criteria = TagLink.createCriteria() - def instance =this + def instance = this def link = criteria.get { criteria.tag { if (!Tag.preserveCase) { @@ -87,42 +91,40 @@ trait Taggable { criteria.eq 'tagRef', instance.id criteria.eq 'type', GrailsNameUtils.getPropertyName(instance.class) criteria.cache true - } - link?.delete(flush:true) - return this + } + link?.delete(flush: true) + return this } - Taggable setTags( List tags ) { + Taggable setTags(List tags) { // remove invalid tags - tags = tags?.findAll { it } + tags = tags?.findAll { it } if (tags) { // remove old tags that not appear in the new tags - getTagLinks(Holders.applicationContext.taggableService, this)*.each { TagLink tagLink -> + getTagLinks(this)*.each { TagLink tagLink -> if (tags.contains(tagLink.tag.name)) { tags.remove(tagLink.tag.name) } else { - tagLink.delete(flush:true) // Grails >=3.3.0 requires flush + tagLink.delete(flush: true) // Grails >=3.3.0 requires flush } } // add the rest addTags(tags) } else { - getTagLinks(Holders.applicationContext.taggableService, this)*.delete(flush:true) + getTagLinks(this)*.delete(flush: true) } return this } - - static List getAllTags() { - def criteria = TagLink.createCriteria() + def criteria = TagLink.createCriteria() criteria.list { criteria.projections { criteria.tag { criteria.distinct "name" } } - criteria.'in'('type', (Collection)Holders.applicationContext.taggableService.domainClassFamilies[this.name]) + criteria.'in'('type', (Collection) Holders.applicationContext.getBean(TaggableService).domainClassFamilies[this.name]) criteria.cache true - } + } as List } static Integer getTotalTags() { @@ -130,13 +132,14 @@ trait Taggable { def criteria = TagLink.createCriteria() criteria.get { criteria.projections { criteria.tag { criteria.countDistinct "name" } } - criteria.'in'('type', (Collection)Holders.applicationContext.taggableService.domainClassFamilies[clazz.name]) + criteria.'in'('type', (Collection) Holders.applicationContext.getBean(TaggableService).domainClassFamilies[clazz.name]) criteria.cache true - } + } as Integer } - static Integer countByTag( String tag ) { - def identifiers = getTagReferences(Holders.applicationContext.taggableService, tag, this.name) - if(identifiers) { + + static Integer countByTag(String tag) { + def identifiers = getTagReferences(tag, this.name) + if (identifiers) { def criteria = createCriteria() criteria.get { criteria.projections { @@ -145,56 +148,53 @@ trait Taggable { criteria.inList 'id', identifiers criteria.cache true } - } - else { - return 0 + } else { + return 0 } } - - static List findAllByTag( String name ) { - def identifiers = getTagReferences(Holders.applicationContext.taggableService, name, this.name) - if(identifiers) { - return findAllByIdInList(identifiers, [cache:true]) - } - else { - return Collections.EMPTY_LIST + + static List findAllByTag(String name) { + def identifiers = getTagReferences(name, this.name) + if (identifiers) { + return findAllByIdInList(identifiers, [cache: true]) + } else { + return Collections.EMPTY_LIST } } - static List findAllByTag ( String name, Map args ) { - def identifiers = getTagReferences(Holders.applicationContext.taggableService, name, this.name) - if(identifiers) { - args.cache=true + static List findAllByTag(String name, Map args) { + def identifiers = getTagReferences(name, this.name) + if (identifiers) { + args.cache = true return findAllByIdInList(identifiers, args) - } - else { - return Collections.EMPTY_LIST + } else { + return Collections.EMPTY_LIST } } - static List findAllByTagWithCriteria( String name, Closure crit ) { + static List findAllByTagWithCriteria(String name, Closure crit) { def clazz = this - def identifiers = getTagReferences(Holders.applicationContext.taggableService, name, clazz.name) - if(identifiers) { + def identifiers = getTagReferences(name, clazz.name) + if (identifiers) { return clazz.withCriteria { 'in'('id', identifiers) crit.delegate = delegate crit.call() } - } - else { + } else { return Collections.EMPTY_LIST } } - static List findAllTagsWithCriteria( Map params, Closure crit ) { + + static List findAllTagsWithCriteria(Map params, Closure crit) { def clazz = this - def criteria = TagLink.createCriteria() + BuildableCriteria criteria = TagLink.createCriteria() criteria.list { - criteria.projections { criteria.tag { criteria.distinct "name" } } - criteria.'in'('type', Holders.applicationContext.taggableService.domainClassFamilies[clazz.name]) - criteria.cache true - criteria.tag(crit) + criteria.projections { criteria.tag { criteria.distinct "name" } } + criteria.'in'('type', Holders.applicationContext.getBean(TaggableService).domainClassFamilies[clazz.name]) + criteria.cache true + criteria.tag(crit) if (params.offset != null) { criteria.firstResult(params.offset.toInteger()) @@ -205,31 +205,33 @@ trait Taggable { criteria.tag { criteria.order('name', 'asc') } - } + } as List } - private getTagLinks(tagService, obj) { - TagLink.findAllByTagRefAndTypeInList(obj.id, tagService.domainClassFamilies[obj.class.name], [cache:true]) + + private getTagLinks(obj) { + TaggableService tagService = Holders.applicationContext.getBean(TaggableService) + TagLink.findAllByTagRefAndTypeInList(obj.id, tagService.domainClassFamilies[obj.class.name], [cache: true]) } - private static getTagReferences(tagService, String tagName, String className) { - if(tagName) { - def criteria = TagLink.createCriteria() + private static getTagReferences(String tagName, String className) { + TaggableService tagService = Holders.applicationContext.getBean(TaggableService) + if (tagName) { + def criteria = TagLink.createCriteria() criteria.list { criteria.projections { criteria.property 'tagRef' } criteria.tag { - criteria.eq 'name', tagName - } + criteria.eq 'name', tagName + } criteria.'in'('type', tagService.domainClassFamilies[className]) criteria.cache true } - - } - else { + + } else { return Collections.EMPTY_LIST - } - } + } + } -} \ No newline at end of file +} diff --git a/src/test/groovy/grails/plugins/taggable/TagsTagLibSpec.groovy b/plugin/src/test/groovy/grails/plugins/taggable/TagsTagLibSpec.groovy similarity index 61% rename from src/test/groovy/grails/plugins/taggable/TagsTagLibSpec.groovy rename to plugin/src/test/groovy/grails/plugins/taggable/TagsTagLibSpec.groovy index a648ec7..10f3ba5 100644 --- a/src/test/groovy/grails/plugins/taggable/TagsTagLibSpec.groovy +++ b/plugin/src/test/groovy/grails/plugins/taggable/TagsTagLibSpec.groovy @@ -6,34 +6,32 @@ import spock.lang.Specification class TagsTagLibSpec extends Specification implements TagLibUnitTest { - def setup() { + void setup() { webRequest.controllerName = 'default' config.setAt("grails.taggable.css.classes", null) } void "test that tagCloud fails when no action attribute specified"() { when: - tagLib.tagCloud tags: [:] + applyTemplate('', [tags: [:]]) then: Exception e = thrown(GrailsTagException) - e != null - e.getMessage() == "Required attribute [action] is missing" + e.getMessage().endsWith( "Required attribute [action] is missing") } void "test that tagCloud fails when no tags attribute specified"() { when: - tagLib.tagCloud action: "browseByTag" + applyTemplate('', [action: "browseByTag"]) then: Exception e = thrown(GrailsTagException) - e != null - e.getMessage() == "Required attribute [tags] must be a map of tag names to tag counts" + e.getMessage().endsWith("Required attribute [tags] must be a map of tag names to tag counts") } void "test that tagCloud uses current controller when no attr passed"() { when: - def result = tagLib.tagCloud(tags: [hello: 1, world: 10, apple: 3, orange: 7], action: "byTag") + def result = applyTemplate('', [tags: [hello: 1, world: 10, apple: 3, orange: 7], action: "byTag"]) then: - result.toString() == "
  1. " + + result == "
    1. " + "hello
    2. " + "
    3. world
    4. " + "
    5. apple
    6. " + @@ -43,9 +41,9 @@ class TagsTagLibSpec extends Specification implements TagLibUnitTest void "test tagCloud with long counts"() { when: - def result = tagLib.tagCloud(tags: [hello: 1L, world: 10L, apple: 3L, orange: 7L], action: "byTag") + def result = applyTemplate('', [tags: [hello: 1L, world: 10L, apple: 3L, orange: 7L], action: "byTag"]) then: - result.toString() == "
      1. " + + result == "
        1. " + "hello
        2. " + "
        3. world
        4. " + "
        5. apple
        6. " + @@ -55,9 +53,9 @@ class TagsTagLibSpec extends Specification implements TagLibUnitTest void "test tagCloud with controller attribute"() { when: - def result = tagLib.tagCloud(tags: [hello: 1, world: 10, apple: 3, orange: 7], controller: "plugin", action: "byTag") + def result = applyTemplate('', [tags: [hello: 1, world: 10, apple: 3, orange: 7], controller: "plugin", action: "byTag"]) then: - result.toString() == "
          1. " + + result == "
            1. " + "hello
            2. " + "
            3. world
            4. " + "
            5. apple
            6. " + @@ -67,9 +65,9 @@ class TagsTagLibSpec extends Specification implements TagLibUnitTest void "test tagCloud with custom id property"() { when: - def result = tagLib.tagCloud(tags: [hello: 1, world: 10, apple: 3, orange: 7], controller: "plugin", action: "byTag", idProperty: "tagName") + def result = applyTemplate('', [tags: [hello: 1, world: 10, apple: 3, orange: 7], controller: "plugin", action: "byTag", idProperty: "tagName"]) then: - result.toString() == "
              1. " + + result == "
                1. " + "hello
                2. " + "
                3. world
                4. " + "
                5. apple
                6. " + @@ -86,9 +84,9 @@ class TagsTagLibSpec extends Specification implements TagLibUnitTest "four" ] when: - def result = tagLib.tagCloud(tags: [hello: 2, world: 10, apple: 3, orange: 7], controller: "plugin", action: "byTag") + def result = applyTemplate('', [tags: [hello: 2, world: 10, apple: 3, orange: 7], controller: "plugin", action: "byTag"]) then: - result.toString() == "
                  1. " + + result == "
                    1. " + "hello
                    2. " + "
                    3. world
                    4. " + "
                    5. apple
                    6. " + diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..895b1d8 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,57 @@ +import org.gradle.api.initialization.resolve.RepositoriesMode + +pluginManagement { + repositories { + if (System.getenv('INCLUDE_MAVEN_LOCAL')) { + mavenLocal() + } + maven { url = 'https://repo.grails.org/grails/restricted' } + } + + includeBuild('./build-logic') { + it.name = 'build-logic' + } +} + +plugins { + id 'com.gradle.common-custom-user-data-gradle-plugin' version '2.3' +} + +def isCI = System.getenv().containsKey('CI') +def isLocal = !isCI +def isReproducibleBuild = System.getenv('SOURCE_DATE_EPOCH') != null +if (isReproducibleBuild) { + gradle.settingsEvaluated { + logger.warn( + '***** Remote Build Cache Disabled due to Reproducible Build *****\n' + + 'Build date will be set to (SOURCE_DATE_EPOCH={})', + System.getenv('SOURCE_DATE_EPOCH') + ) + } +} + +buildCache { + local { enabled = (isLocal && !isReproducibleBuild) || (isCI && isReproducibleBuild) } +} + +rootProject.name = 'taggable-root' + +include('plugin') +project(':plugin').name = 'taggable' +include('docs') +project(':docs').name = 'taggable-docs' + +file('examples').listFiles({ it.directory } as FileFilter).each { + include(it.name) + project(":$it.name").projectDir = file("examples/$it.name") +} + +dependencyResolutionManagement { + repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS + repositories { + if (System.getenv('INCLUDE_MAVEN_LOCAL')) { + mavenLocal() + } + maven { url = 'https://repo.grails.org/grails/restricted' } + } +} diff --git a/src/integration-test/groovy/grails/plugins/taggable/TaggableSpec.groovy b/src/integration-test/groovy/grails/plugins/taggable/TaggableSpec.groovy deleted file mode 100644 index 7f6d737..0000000 --- a/src/integration-test/groovy/grails/plugins/taggable/TaggableSpec.groovy +++ /dev/null @@ -1,298 +0,0 @@ -package grails.plugins.taggable - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration -import spock.lang.Specification - -import static org.junit.Assert.assertEquals -import static org.junit.Assert.assertTrue - -@Integration -@Rollback -class TaggableSpec extends Specification { - - def setup() { - Tag.preserveCaseForTesting = false - } - - void testAddTagMethodCaseInsensitive() { - given: - def td = new TestDomain(name: "foo") - td.save() - - when: - td.addTag("Groovy") - .addTag("grails") - and: - def links = TagLink.findAllWhere(tagRef: td.id, type: 'testDomain') - - then: - assertEquals 2, links.size() - assertEquals(['groovy', 'grails'], links.tag.name) - } - - void testAddTagMethodCasePreserving() { - given: - Tag.preserveCaseForTesting = true - - def td = new TestDomain(name:"foo") - td.save() - - when: - td.addTag("Groovy") - .addTag("grails") - - def links = TagLink.findAllWhere(tagRef:td.id, type:'testDomain') - then: - assertEquals 2, links.size() - assertEquals( ['Groovy', 'grails'], links.tag.name ) - - when: - // adding a second, even if preserving case in DB it should still not add it as already has such a tag - td.addTag("groovy") - and: - links = TagLink.findAllWhere(tagRef:td.id, type:'testDomain') - - then: - assertEquals 3, links.size() - assertEquals( ['Groovy', 'grails', 'groovy'], links.tag.name ) - } - - void testAddTagsMethod() { - given: - def td = new TestDomain(name:"foo") - td.save() - - when: - td.addTags(["groovy","grails"]) - and: - def links = TagLink.findAllWhere(tagRef:td.id, type:'testDomain') - - then: - assertEquals 2, links.size() - assertEquals( ['groovy', 'grails'], links.tag.name ) - } - - void testRemoveTagMethod() { - given: - def td = new TestDomain(name: "foo") - td.save() - - when: - td.addTag("groovy") - .addTag("grails") - and: - def links = TagLink.findAllWhere(tagRef: td.id, type: 'testDomain') - - then: - assertEquals 2, links.size() - assertEquals(['groovy', 'grails'], links.tag.name) - - when: - td.removeTag("groovy") - and: - links = TagLink.findAllWhere(tagRef: td.id, type: 'testDomain') - - then: - assertEquals 1, links.size() - assertEquals(['grails'], links.tag.name) - } - - void testGetTagsMethod() { - given: - def td = new TestDomain(name:"foo") - td.save() - - when: - td.addTag("groovy") - .addTag("grails") - td.save(flush:true) - - and: - TestDomain.withSession { session -> session.clear() } - td = TestDomain.findByName("foo") - - then: - assertEquals( ['groovy', 'grails'], td.tags ) - } - - void testSetTagsMethod() { - given: - def td = new TestDomain(name:"foo") - td.save() - - when: - td.tags = ["groovy", null, "grails", ''] - and: - def links = TagLink.findAllWhere(tagRef:td.id, type:'testDomain') - - then: - assertEquals 2, links.size() - assertEquals( ['groovy', 'grails'], links.tag.name ) - assertEquals( ['groovy', 'grails'], td.tags ) - - when: - td.tags = ["foo", "bar"] - and: - links = TagLink.findAllWhere(tagRef:td.id, type:'testDomain') - - then: - assertEquals 2, links.size() - assertEquals( ['foo', 'bar'].sort(true), links.tag.name.sort(true) ) - assertEquals( ['foo', 'bar'].sort(true), td.tags.sort(true) ) - - when: - td.tags = [] - and: - links = TagLink.findAllWhere(tagRef:td.id, type:'testDomain') - - then: - assertEquals 0, links.size() - assertEquals( [], links.tag.name ) - assertEquals( [], td.tags ) - } - - void testFindAllByTag() { - given: - new TestDomain(name:"foo") - .save() - .addTag("groovy") - .addTag("grails") - .addTag("griffon") - new TestDomain(name:"bar") - .save() - .addTag("groovy") - .addTag("grails") - - when: - def results = TestDomain.findAllByTag("groovy") - - then: - assertEquals 2, results.size() - assertTrue results[0] instanceof TestDomain - - assertEquals 2, TestDomain.findAllByTag("groovy").size() - assertEquals 2, TestDomain.findAllByTag("grails").size() - assertEquals 1, TestDomain.findAllByTag("griffon").size() - assertEquals 0, TestDomain.findAllByTag("nothing").size() - assertEquals 0, TestDomain.findAllByTag(null).size() - - } - - void testFindAllByTagPolymorphic() { - given: - new TestDomain(name:"foo") - .save() - .addTag("groovy") - .addTag("grails") - .addTag("griffon") - new TestDescendent(name:"bar", other:'bla') - .save() - .addTag("groovy") - .addTag("grails") - .addTag("gradle") - - when: - def results = TestDomain.findAllByTag("groovy") - - then: - assertEquals 2, results.size() - assertTrue results[0] instanceof TestDomain - - assertEquals 2, TestDomain.findAllByTag("groovy").size() - assertEquals 1, TestDescendent.findAllByTag("groovy").size() - - assertEquals 2, TestDomain.findAllByTag("grails").size() - assertEquals 1, TestDescendent.findAllByTag("grails").size() - - assertEquals 1, TestDomain.findAllByTag("gradle").size() - assertEquals 1, TestDescendent.findAllByTag("gradle").size() - - assertEquals 1, TestDomain.findAllByTag("griffon").size() - assertEquals 0, TestDomain.findAllByTag("nothing").size() - assertEquals 0, TestDomain.findAllByTag(null).size() - - } - - void testCountByTag() { - given: - new TestDomain(name:"foo") - .save() - .addTag("groovy") - .addTag("grails") - .addTag("griffon") - new TestDomain(name:"bar") - .save() - .addTag("groovy") - .addTag("grails") - new TestDescendent(name:"bla", other:'zzzz') - .save() - .addTag("groovy") - .addTag("grails") - .addTag("gradle") - - expect: - assertEquals 3, TestDomain.countByTag("groovy") - assertEquals 1, TestDescendent.countByTag("groovy") - - assertEquals 1, TestDomain.countByTag("griffon") - assertEquals 0, TestDescendent.countByTag("griffon") - - assertEquals 1, TestDomain.countByTag("gradle") - assertEquals 1, TestDescendent.countByTag("gradle") - - assertEquals 0, TestDomain.countByTag("rubbish") - assertEquals 0, TestDomain.countByTag(null) - - } - - void testAllTags() { - given: - new TestDomain(name:"foo") - .save() - .addTag("groovy") - .addTag("grails") - .addTag("griffon") - new TestDomain(name:"bar") - .save() - .addTag("groovy") - .addTag("grails") - new TestDescendent(name:"bla", other:'zzzz') - .save() - .addTag("groovy") - .addTag("grails") - .addTag("gradle") - - expect: - assertEquals( ['gradle','grails','griffon','groovy'].sort(true), TestDomain.allTags.sort(true) ) - assertEquals 4, TestDomain.totalTags - - assertEquals( ['gradle','grails','groovy'].sort(true), TestDescendent.allTags.sort(true) ) - assertEquals 3, TestDescendent.totalTags - } - - void testParseTags() { - given: - def td = new TestDomain(name:"foo") - .save() - - when: - td.parseTags("groovy,grails,griffon") - - then: - assertEquals( ['grails','griffon','groovy'], TestDomain.allTags ) - } - - void testParseTagsWithDelimiter() { - given: - def td = new TestDomain(name:"foo") - .save() - - when: - td.parseTags("groovy grails griffon", " ") - - then: - assertEquals( ['grails','griffon','groovy'], TestDomain.allTags ) - - } -} diff --git a/travis-build.sh b/travis-build.sh deleted file mode 100755 index abe9f5d..0000000 --- a/travis-build.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -set -e -rm -rf *.zip -./gradlew clean test assemble - -filename=$(find build/libs -name "*.jar" | head -1) -filename=$(basename "$filename") - -EXIT_STATUS=0 -echo "Publishing archives for branch $TRAVIS_BRANCH" -if [[ -n $TRAVIS_TAG ]] || [[ $TRAVIS_BRANCH == 'master' && $TRAVIS_PULL_REQUEST == 'false' ]]; then - - echo "Publishing archives" - - if [[ -n $TRAVIS_TAG ]]; then - ./gradlew bintrayUpload || EXIT_STATUS=$? - else - ./gradlew publish || EXIT_STATUS=$? - fi - -fi -exit $EXIT_STATUS \ No newline at end of file From 48b8aac3e3fe652cfe5d11e4dcdb26abfabef7df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Berg=20Glasius?= Date: Wed, 22 Apr 2026 14:16:42 +0200 Subject: [PATCH 2/6] Remove Travis CI and GitHub Actions configuration, add .editorconfig file * Made into multi-build project * Fixed tests * Cleaned up Taggable * Cleaned up TagLib descriptor --- .editorconfig | 1393 +++++++++++++++++ .github/workflows/build.yml | 28 - .github/workflows/ci.yml | 110 ++ .github/workflows/release-notes.yml | 26 + .github/workflows/release.yml | 281 +++- .sdkmanrc | 3 + .skills/gradle-best-practices.md | 254 +++ .skills/plugin-project.md | 145 ++ .skills/repository-structure.md | 216 +++ .travis.yml | 19 - AGENTS.md | 202 +++ CLAUDE.md | 1 + CONTRIBUTING.md | 163 ++ README.md | 9 +- build-logic/build.gradle | 29 + build-logic/settings.gradle | 13 + .../src/main/groovy/config.app-run.gradle | 12 + .../src/main/groovy/config.compile.gradle | 65 + .../src/main/groovy/config.docs.gradle | 110 ++ .../src/main/groovy/config.example-app.gradle | 8 + .../main/groovy/config.grails-assets.gradle | 26 + .../main/groovy/config.grails-plugin.gradle | 16 + .../main/groovy/config.publish-root.gradle | 16 + .../src/main/groovy/config.publish.gradle | 9 + .../src/main/groovy/config.testing.gradle | 45 + build.gradle | 191 +-- docs/build.gradle | 41 + docs/src/docs/gettingStarted.adoc | 52 + docs/src/docs/index.adoc | 8 + docs/src/docs/index.tmpl | 389 +++++ docs/src/docs/introduction.adoc | 12 + .../src/docs/introduction/currentVersion.adoc | 14 + docs/src/docs/introduction/license.adoc | 6 + docs/src/docs/introduction/sourceCode.adoc | 12 + docs/src/docs/usage.adoc | 93 ++ examples/app1/build.gradle | 47 + .../assets/stylesheets/application.css | 5 + examples/app1/grails-app/conf/application.yml | 93 ++ .../app1/grails-app}/conf/logback.xml | 0 .../plugins/taggable/TestDescendent.groovy | 0 .../grails/plugins/taggable/TestDomain.groovy | 0 .../plugins/taggable/Application.groovy | 5 +- .../app1/grails-app/views/layouts/main.gsp | 13 + .../plugins/taggable/TaggableSpec.groovy | 283 ++++ gradle.properties | 23 +- gradle/wrapper/gradle-wrapper.jar | Bin 52818 -> 43764 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 309 ++-- gradlew.bat | 90 +- grails-app/conf/application.yml | 110 -- plugin/build.gradle | 51 + plugin/grails-app/assets/images/.gitkeep | 0 plugin/grails-app/conf/application.yml | 10 + .../domain/grails/plugins/taggable/Tag.groovy | 0 .../grails/plugins/taggable/TagLink.groovy | 0 .../plugins/taggable/TaggableService.groovy | 36 +- .../grails/plugins/taggable/TagsTagLib.groovy | 0 .../plugins/taggable/TagException.groovy | 0 .../grails/plugins/taggable/Taggable.groovy | 172 +- .../taggable/TaggableGrailsPlugin.groovy | 14 +- .../plugins/taggable/TagsTagLibSpec.groovy | 32 +- settings.gradle | 57 + .../plugins/taggable/TaggableSpec.groovy | 298 ---- travis-build.sh | 22 - 64 files changed, 4638 insertions(+), 1053 deletions(-) create mode 100644 .editorconfig delete mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release-notes.yml create mode 100644 .sdkmanrc create mode 100644 .skills/gradle-best-practices.md create mode 100644 .skills/plugin-project.md create mode 100644 .skills/repository-structure.md delete mode 100644 .travis.yml create mode 100644 AGENTS.md create mode 120000 CLAUDE.md create mode 100644 CONTRIBUTING.md create mode 100644 build-logic/build.gradle create mode 100644 build-logic/settings.gradle create mode 100644 build-logic/src/main/groovy/config.app-run.gradle create mode 100644 build-logic/src/main/groovy/config.compile.gradle create mode 100644 build-logic/src/main/groovy/config.docs.gradle create mode 100644 build-logic/src/main/groovy/config.example-app.gradle create mode 100644 build-logic/src/main/groovy/config.grails-assets.gradle create mode 100644 build-logic/src/main/groovy/config.grails-plugin.gradle create mode 100644 build-logic/src/main/groovy/config.publish-root.gradle create mode 100644 build-logic/src/main/groovy/config.publish.gradle create mode 100644 build-logic/src/main/groovy/config.testing.gradle create mode 100644 docs/build.gradle create mode 100644 docs/src/docs/gettingStarted.adoc create mode 100644 docs/src/docs/index.adoc create mode 100644 docs/src/docs/index.tmpl create mode 100644 docs/src/docs/introduction.adoc create mode 100644 docs/src/docs/introduction/currentVersion.adoc create mode 100644 docs/src/docs/introduction/license.adoc create mode 100644 docs/src/docs/introduction/sourceCode.adoc create mode 100644 docs/src/docs/usage.adoc create mode 100644 examples/app1/build.gradle create mode 100644 examples/app1/grails-app/assets/stylesheets/application.css create mode 100644 examples/app1/grails-app/conf/application.yml rename {grails-app => examples/app1/grails-app}/conf/logback.xml (100%) rename {grails-app => examples/app1/grails-app}/domain/grails/plugins/taggable/TestDescendent.groovy (100%) rename {grails-app => examples/app1/grails-app}/domain/grails/plugins/taggable/TestDomain.groovy (100%) rename {grails-app => examples/app1/grails-app}/init/grails/plugins/taggable/Application.groovy (80%) create mode 100644 examples/app1/grails-app/views/layouts/main.gsp create mode 100644 examples/app1/src/integration-test/groovy/grails/plugins/taggable/TaggableSpec.groovy delete mode 100644 grails-app/conf/application.yml create mode 100644 plugin/build.gradle create mode 100644 plugin/grails-app/assets/images/.gitkeep create mode 100644 plugin/grails-app/conf/application.yml rename {grails-app => plugin/grails-app}/domain/grails/plugins/taggable/Tag.groovy (100%) rename {grails-app => plugin/grails-app}/domain/grails/plugins/taggable/TagLink.groovy (100%) rename {grails-app => plugin/grails-app}/services/grails/plugins/taggable/TaggableService.groovy (51%) rename {grails-app => plugin/grails-app}/taglib/grails/plugins/taggable/TagsTagLib.groovy (100%) rename {src => plugin/src}/main/groovy/grails/plugins/taggable/TagException.groovy (100%) rename {src => plugin/src}/main/groovy/grails/plugins/taggable/Taggable.groovy (54%) rename {src => plugin/src}/main/groovy/grails/plugins/taggable/TaggableGrailsPlugin.groovy (81%) rename {src => plugin/src}/test/groovy/grails/plugins/taggable/TagsTagLibSpec.groovy (61%) create mode 100644 settings.gradle delete mode 100644 src/integration-test/groovy/grails/plugins/taggable/TaggableSpec.groovy delete mode 100755 travis-build.sh diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1de0688 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,1393 @@ +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +max_line_length = 120 +tab_width = 4 +ij_continuation_indent_size = 8 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = true +ij_smart_tabs = false +ij_visual_guides = +ij_wrap_on_typing = false + +[*.bazelproject] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 2 +ij_projectview_keep_indents_on_empty_lines = false + +[*.css] +ij_css_align_closing_brace_with_properties = false +ij_css_blank_lines_around_nested_selector = 1 +ij_css_blank_lines_between_blocks = 1 +ij_css_block_comment_add_space = false +ij_css_brace_placement = end_of_line +ij_css_enforce_quotes_on_format = false +ij_css_hex_color_long_format = false +ij_css_hex_color_lower_case = false +ij_css_hex_color_short_format = false +ij_css_hex_color_upper_case = false +ij_css_keep_blank_lines_in_code = 2 +ij_css_keep_indents_on_empty_lines = false +ij_css_keep_single_line_blocks = false +ij_css_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow +ij_css_space_after_colon = true +ij_css_space_before_opening_brace = true +ij_css_use_double_quotes = true +ij_css_value_alignment = do_not_align + +[*.dcl] +ij_declarative_keep_indents_on_empty_lines = false + +[*.gsp] +ij_gsp_keep_indents_on_empty_lines = false + +[*.java] +ij_java_align_consecutive_assignments = false +ij_java_align_consecutive_variable_declarations = false +ij_java_align_group_field_declarations = false +ij_java_align_multiline_annotation_parameters = false +ij_java_align_multiline_array_initializer_expression = false +ij_java_align_multiline_assignment = false +ij_java_align_multiline_binary_operation = false +ij_java_align_multiline_chained_methods = false +ij_java_align_multiline_deconstruction_list_components = true +ij_java_align_multiline_extends_list = false +ij_java_align_multiline_for = true +ij_java_align_multiline_method_parentheses = false +ij_java_align_multiline_parameters = true +ij_java_align_multiline_parameters_in_calls = false +ij_java_align_multiline_parenthesized_expression = false +ij_java_align_multiline_records = true +ij_java_align_multiline_resources = true +ij_java_align_multiline_ternary_operation = false +ij_java_align_multiline_text_blocks = false +ij_java_align_multiline_throws_list = false +ij_java_align_subsequent_simple_methods = false +ij_java_align_throws_keyword = false +ij_java_align_types_in_multi_catch = true +ij_java_annotation_new_line_in_record_component = false +ij_java_annotation_parameter_wrap = off +ij_java_array_initializer_new_line_after_left_brace = false +ij_java_array_initializer_right_brace_on_new_line = false +ij_java_array_initializer_wrap = off +ij_java_assert_statement_colon_on_next_line = false +ij_java_assert_statement_wrap = off +ij_java_assignment_wrap = off +ij_java_binary_operation_sign_on_next_line = false +ij_java_binary_operation_wrap = off +ij_java_blank_lines_after_anonymous_class_header = 0 +ij_java_blank_lines_after_class_header = 0 +ij_java_blank_lines_after_imports = 1 +ij_java_blank_lines_after_package = 1 +ij_java_blank_lines_around_class = 1 +ij_java_blank_lines_around_field = 0 +ij_java_blank_lines_around_field_in_interface = 0 +ij_java_blank_lines_around_field_with_annotations = 0 +ij_java_blank_lines_around_initializer = 1 +ij_java_blank_lines_around_method = 1 +ij_java_blank_lines_around_method_in_interface = 1 +ij_java_blank_lines_before_class_end = 0 +ij_java_blank_lines_before_imports = 1 +ij_java_blank_lines_before_method_body = 0 +ij_java_blank_lines_before_package = 0 +ij_java_blank_lines_between_record_components = 0 +ij_java_block_brace_style = end_of_line +ij_java_block_comment_add_space = false +ij_java_block_comment_at_first_column = true +ij_java_builder_methods = +ij_java_call_parameters_new_line_after_left_paren = false +ij_java_call_parameters_right_paren_on_new_line = false +ij_java_call_parameters_wrap = off +ij_java_case_statement_on_separate_line = true +ij_java_catch_on_new_line = false +ij_java_class_annotation_wrap = split_into_lines +ij_java_class_brace_style = end_of_line +ij_java_class_count_to_use_import_on_demand = 999 +ij_java_class_names_in_javadoc = 1 +ij_java_deconstruction_list_wrap = normal +ij_java_delete_unused_module_imports = false +ij_java_do_not_indent_top_level_class_members = false +ij_java_do_not_wrap_after_single_annotation = false +ij_java_do_not_wrap_after_single_annotation_in_parameter = false +ij_java_do_while_brace_force = never +ij_java_doc_add_blank_line_after_description = true +ij_java_doc_add_blank_line_after_param_comments = false +ij_java_doc_add_blank_line_after_return = false +ij_java_doc_add_p_tag_on_empty_lines = true +ij_java_doc_align_exception_comments = true +ij_java_doc_align_param_comments = true +ij_java_doc_do_not_wrap_if_one_line = false +ij_java_doc_enable_formatting = true +ij_java_doc_enable_leading_asterisks = true +ij_java_doc_indent_on_continuation = false +ij_java_doc_keep_empty_lines = true +ij_java_doc_keep_empty_parameter_tag = true +ij_java_doc_keep_empty_return_tag = true +ij_java_doc_keep_empty_throws_tag = true +ij_java_doc_keep_invalid_tags = true +ij_java_doc_param_description_on_new_line = false +ij_java_doc_preserve_line_breaks = false +ij_java_doc_use_throws_not_exception_tag = true +ij_java_else_on_new_line = false +ij_java_entity_dd_prefix = +ij_java_entity_dd_suffix = EJB +ij_java_entity_eb_prefix = +ij_java_entity_eb_suffix = Bean +ij_java_entity_hi_prefix = +ij_java_entity_hi_suffix = Home +ij_java_entity_lhi_prefix = Local +ij_java_entity_lhi_suffix = Home +ij_java_entity_li_prefix = Local +ij_java_entity_li_suffix = +ij_java_entity_pk_class = java.lang.String +ij_java_entity_ri_prefix = +ij_java_entity_ri_suffix = +ij_java_entity_vo_prefix = +ij_java_entity_vo_suffix = VO +ij_java_enum_constants_wrap = off +ij_java_enum_field_annotation_wrap = off +ij_java_extends_keyword_wrap = off +ij_java_extends_list_wrap = off +ij_java_field_annotation_wrap = split_into_lines +ij_java_field_name_prefix = +ij_java_field_name_suffix = +ij_java_filter_class_prefix = +ij_java_filter_class_suffix = +ij_java_filter_dd_prefix = +ij_java_filter_dd_suffix = +ij_java_finally_on_new_line = false +ij_java_for_brace_force = never +ij_java_for_statement_new_line_after_left_paren = false +ij_java_for_statement_right_paren_on_new_line = false +ij_java_for_statement_wrap = off +ij_java_generate_final_locals = false +ij_java_generate_final_parameters = false +ij_java_generate_use_type_annotation_before_type = true +ij_java_if_brace_force = never +ij_java_imports_layout = java.**, |, javax.**, |, groovy.**, org.apache.groovy.**, org.codehaus.groovy.**, |, jakarta.**, |, *, |, io.spring.**, org.springframework.**, |, grails.**, org.apache.grails.**, org.grails.**, |, $* +ij_java_indent_case_from_switch = true +ij_java_insert_inner_class_imports = false +ij_java_insert_override_annotation = true +ij_java_keep_blank_lines_before_right_brace = 2 +ij_java_keep_blank_lines_between_package_declaration_and_header = 2 +ij_java_keep_blank_lines_in_code = 2 +ij_java_keep_blank_lines_in_declarations = 2 +ij_java_keep_builder_methods_indents = false +ij_java_keep_control_statement_in_one_line = true +ij_java_keep_first_column_comment = true +ij_java_keep_indents_on_empty_lines = false +ij_java_keep_line_breaks = true +ij_java_keep_multiple_expressions_in_one_line = false +ij_java_keep_simple_blocks_in_one_line = false +ij_java_keep_simple_classes_in_one_line = false +ij_java_keep_simple_lambdas_in_one_line = false +ij_java_keep_simple_methods_in_one_line = false +ij_java_label_indent_absolute = false +ij_java_label_indent_size = 0 +ij_java_lambda_brace_style = end_of_line +ij_java_layout_on_demand_import_from_same_package_first = true +ij_java_layout_static_imports_separately = true +ij_java_line_comment_add_space = false +ij_java_line_comment_add_space_on_reformat = false +ij_java_line_comment_at_first_column = true +ij_java_listener_class_prefix = +ij_java_listener_class_suffix = +ij_java_local_variable_name_prefix = +ij_java_local_variable_name_suffix = +ij_java_message_dd_prefix = +ij_java_message_dd_suffix = EJB +ij_java_message_eb_prefix = +ij_java_message_eb_suffix = Bean +ij_java_method_annotation_wrap = split_into_lines +ij_java_method_brace_style = end_of_line +ij_java_method_call_chain_wrap = off +ij_java_method_parameters_new_line_after_left_paren = false +ij_java_method_parameters_right_paren_on_new_line = false +ij_java_method_parameters_wrap = off +ij_java_modifier_list_wrap = false +ij_java_multi_catch_types_wrap = normal +ij_java_names_count_to_use_import_on_demand = 999 +ij_java_new_line_after_lparen_in_annotation = false +ij_java_new_line_after_lparen_in_deconstruction_pattern = true +ij_java_new_line_after_lparen_in_record_header = false +ij_java_new_line_when_body_is_presented = false +ij_java_packages_to_use_import_on_demand = +ij_java_parameter_annotation_wrap = off +ij_java_parameter_name_prefix = +ij_java_parameter_name_suffix = +ij_java_parentheses_expression_new_line_after_left_paren = false +ij_java_parentheses_expression_right_paren_on_new_line = false +ij_java_place_assignment_sign_on_next_line = false +ij_java_prefer_longer_names = true +ij_java_prefer_parameters_wrap = false +ij_java_preserve_module_imports = true +ij_java_record_components_wrap = normal +ij_java_repeat_annotations = +ij_java_repeat_synchronized = true +ij_java_replace_instanceof_and_cast = false +ij_java_replace_null_check = true +ij_java_replace_sum_lambda_with_method_ref = true +ij_java_resource_list_new_line_after_left_paren = false +ij_java_resource_list_right_paren_on_new_line = false +ij_java_resource_list_wrap = off +ij_java_rparen_on_new_line_in_annotation = false +ij_java_rparen_on_new_line_in_deconstruction_pattern = true +ij_java_rparen_on_new_line_in_record_header = false +ij_java_servlet_class_prefix = +ij_java_servlet_class_suffix = +ij_java_servlet_dd_prefix = +ij_java_servlet_dd_suffix = +ij_java_session_dd_prefix = +ij_java_session_dd_suffix = EJB +ij_java_session_eb_prefix = +ij_java_session_eb_suffix = Bean +ij_java_session_hi_prefix = +ij_java_session_hi_suffix = Home +ij_java_session_lhi_prefix = Local +ij_java_session_lhi_suffix = Home +ij_java_session_li_prefix = Local +ij_java_session_li_suffix = +ij_java_session_ri_prefix = +ij_java_session_ri_suffix = +ij_java_session_si_prefix = +ij_java_session_si_suffix = Service +ij_java_space_after_closing_angle_bracket_in_type_argument = false +ij_java_space_after_colon = true +ij_java_space_after_comma = true +ij_java_space_after_comma_in_type_arguments = true +ij_java_space_after_for_semicolon = true +ij_java_space_after_quest = true +ij_java_space_after_type_cast = true +ij_java_space_before_annotation_array_initializer_left_brace = false +ij_java_space_before_annotation_parameter_list = false +ij_java_space_before_array_initializer_left_brace = false +ij_java_space_before_catch_keyword = true +ij_java_space_before_catch_left_brace = true +ij_java_space_before_catch_parentheses = true +ij_java_space_before_class_left_brace = true +ij_java_space_before_colon = true +ij_java_space_before_colon_in_foreach = true +ij_java_space_before_comma = false +ij_java_space_before_deconstruction_list = false +ij_java_space_before_do_left_brace = true +ij_java_space_before_else_keyword = true +ij_java_space_before_else_left_brace = true +ij_java_space_before_finally_keyword = true +ij_java_space_before_finally_left_brace = true +ij_java_space_before_for_left_brace = true +ij_java_space_before_for_parentheses = true +ij_java_space_before_for_semicolon = false +ij_java_space_before_if_left_brace = true +ij_java_space_before_if_parentheses = true +ij_java_space_before_method_call_parentheses = false +ij_java_space_before_method_left_brace = true +ij_java_space_before_method_parentheses = false +ij_java_space_before_opening_angle_bracket_in_type_parameter = false +ij_java_space_before_quest = true +ij_java_space_before_switch_left_brace = true +ij_java_space_before_switch_parentheses = true +ij_java_space_before_synchronized_left_brace = true +ij_java_space_before_synchronized_parentheses = true +ij_java_space_before_try_left_brace = true +ij_java_space_before_try_parentheses = true +ij_java_space_before_type_parameter_list = false +ij_java_space_before_while_keyword = true +ij_java_space_before_while_left_brace = true +ij_java_space_before_while_parentheses = true +ij_java_space_inside_one_line_enum_braces = false +ij_java_space_within_empty_array_initializer_braces = false +ij_java_space_within_empty_method_call_parentheses = false +ij_java_space_within_empty_method_parentheses = false +ij_java_spaces_around_additive_operators = true +ij_java_spaces_around_annotation_eq = true +ij_java_spaces_around_assignment_operators = true +ij_java_spaces_around_bitwise_operators = true +ij_java_spaces_around_equality_operators = true +ij_java_spaces_around_lambda_arrow = true +ij_java_spaces_around_logical_operators = true +ij_java_spaces_around_method_ref_dbl_colon = false +ij_java_spaces_around_multiplicative_operators = true +ij_java_spaces_around_relational_operators = true +ij_java_spaces_around_shift_operators = true +ij_java_spaces_around_type_bounds_in_type_parameters = true +ij_java_spaces_around_unary_operator = false +ij_java_spaces_inside_block_braces_when_body_is_present = false +ij_java_spaces_within_angle_brackets = false +ij_java_spaces_within_annotation_parentheses = false +ij_java_spaces_within_array_initializer_braces = false +ij_java_spaces_within_braces = false +ij_java_spaces_within_brackets = false +ij_java_spaces_within_cast_parentheses = false +ij_java_spaces_within_catch_parentheses = false +ij_java_spaces_within_deconstruction_list = false +ij_java_spaces_within_for_parentheses = false +ij_java_spaces_within_if_parentheses = false +ij_java_spaces_within_method_call_parentheses = false +ij_java_spaces_within_method_parentheses = false +ij_java_spaces_within_parentheses = false +ij_java_spaces_within_record_header = false +ij_java_spaces_within_switch_parentheses = false +ij_java_spaces_within_synchronized_parentheses = false +ij_java_spaces_within_try_parentheses = false +ij_java_spaces_within_while_parentheses = false +ij_java_special_else_if_treatment = true +ij_java_static_field_name_prefix = +ij_java_static_field_name_suffix = +ij_java_subclass_name_prefix = +ij_java_subclass_name_suffix = Impl +ij_java_switch_expressions_wrap = normal +ij_java_ternary_operation_signs_on_next_line = false +ij_java_ternary_operation_wrap = off +ij_java_test_name_prefix = +ij_java_test_name_suffix = Test +ij_java_throws_keyword_wrap = off +ij_java_throws_list_wrap = off +ij_java_use_external_annotations = false +ij_java_use_fq_class_names = false +ij_java_use_relative_indents = false +ij_java_use_single_class_imports = true +ij_java_variable_annotation_wrap = off +ij_java_visibility = public +ij_java_while_brace_force = never +ij_java_while_on_new_line = false +ij_java_wrap_comments = false +ij_java_wrap_first_method_in_call_chain = false +ij_java_wrap_long_lines = false +ij_java_wrap_semicolon_after_call_chain = false + +[*.less] +indent_size = 2 +ij_less_align_closing_brace_with_properties = false +ij_less_blank_lines_around_nested_selector = 1 +ij_less_blank_lines_between_blocks = 1 +ij_less_block_comment_add_space = false +ij_less_brace_placement = 0 +ij_less_enforce_quotes_on_format = false +ij_less_hex_color_long_format = false +ij_less_hex_color_lower_case = false +ij_less_hex_color_short_format = false +ij_less_hex_color_upper_case = false +ij_less_keep_blank_lines_in_code = 2 +ij_less_keep_indents_on_empty_lines = false +ij_less_keep_single_line_blocks = false +ij_less_line_comment_add_space = false +ij_less_line_comment_at_first_column = false +ij_less_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow +ij_less_space_after_colon = true +ij_less_space_before_opening_brace = true +ij_less_use_double_quotes = true +ij_less_value_alignment = 0 + +[*.proto] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 4 +ij_protobuf_keep_blank_lines_in_code = 2 +ij_protobuf_keep_indents_on_empty_lines = false +ij_protobuf_keep_line_breaks = true +ij_protobuf_space_after_comma = true +ij_protobuf_space_before_comma = false +ij_protobuf_spaces_around_assignment_operators = true +ij_protobuf_spaces_within_braces = false +ij_protobuf_spaces_within_brackets = false + +[*.sass] +indent_size = 2 +ij_sass_align_closing_brace_with_properties = false +ij_sass_blank_lines_around_nested_selector = 1 +ij_sass_blank_lines_between_blocks = 1 +ij_sass_brace_placement = 0 +ij_sass_enforce_quotes_on_format = false +ij_sass_hex_color_long_format = false +ij_sass_hex_color_lower_case = false +ij_sass_hex_color_short_format = false +ij_sass_hex_color_upper_case = false +ij_sass_keep_blank_lines_in_code = 2 +ij_sass_keep_indents_on_empty_lines = false +ij_sass_keep_single_line_blocks = false +ij_sass_line_comment_add_space = false +ij_sass_line_comment_at_first_column = false +ij_sass_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow +ij_sass_space_after_colon = true +ij_sass_space_before_opening_brace = true +ij_sass_use_double_quotes = true +ij_sass_value_alignment = 0 + +[*.scss] +indent_size = 2 +ij_scss_align_closing_brace_with_properties = false +ij_scss_blank_lines_around_nested_selector = 1 +ij_scss_blank_lines_between_blocks = 1 +ij_scss_block_comment_add_space = false +ij_scss_brace_placement = 0 +ij_scss_enforce_quotes_on_format = false +ij_scss_hex_color_long_format = false +ij_scss_hex_color_lower_case = false +ij_scss_hex_color_short_format = false +ij_scss_hex_color_upper_case = false +ij_scss_keep_blank_lines_in_code = 2 +ij_scss_keep_indents_on_empty_lines = false +ij_scss_keep_single_line_blocks = false +ij_scss_line_comment_add_space = false +ij_scss_line_comment_at_first_column = false +ij_scss_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow +ij_scss_space_after_colon = true +ij_scss_space_before_opening_brace = true +ij_scss_use_double_quotes = true +ij_scss_value_alignment = 0 + +[*.vue] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 4 +ij_vue_indent_children_of_top_level = template +ij_vue_interpolation_new_line_after_start_delimiter = true +ij_vue_interpolation_new_line_before_end_delimiter = true +ij_vue_interpolation_wrap = off +ij_vue_keep_indents_on_empty_lines = false +ij_vue_spaces_within_interpolation_expressions = true + +[.editorconfig] +ij_editorconfig_align_group_field_declarations = false +ij_editorconfig_space_after_colon = false +ij_editorconfig_space_after_comma = true +ij_editorconfig_space_before_colon = false +ij_editorconfig_space_before_comma = false +ij_editorconfig_spaces_around_assignment_operators = true + +[{*.ad,*.adoc,*.asciidoc,.asciidoctorconfig}] +ij_asciidoc_blank_lines_after_header = 1 +ij_asciidoc_blank_lines_keep_after_header = 1 +ij_asciidoc_formatting_enabled = true +ij_asciidoc_one_sentence_per_line = true + +[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.pom,*.rng,*.tld,*.wadl,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}] +ij_xml_align_attributes = true +ij_xml_align_text = false +ij_xml_attribute_wrap = normal +ij_xml_block_comment_add_space = false +ij_xml_block_comment_at_first_column = true +ij_xml_keep_blank_lines = 2 +ij_xml_keep_indents_on_empty_lines = false +ij_xml_keep_line_breaks = true +ij_xml_keep_line_breaks_in_text = true +ij_xml_keep_whitespaces = false +ij_xml_keep_whitespaces_around_cdata = preserve +ij_xml_keep_whitespaces_inside_cdata = false +ij_xml_line_comment_at_first_column = true +ij_xml_space_after_tag_name = false +ij_xml_space_around_equals_in_attribute = false +ij_xml_space_inside_empty_tag = false +ij_xml_text_wrap = normal + +[{*.ats,*.cts,*.mts,*.ts}] +ij_continuation_indent_size = 4 +ij_typescript_align_imports = false +ij_typescript_align_multiline_array_initializer_expression = false +ij_typescript_align_multiline_binary_operation = false +ij_typescript_align_multiline_chained_methods = false +ij_typescript_align_multiline_extends_list = false +ij_typescript_align_multiline_for = true +ij_typescript_align_multiline_parameters = true +ij_typescript_align_multiline_parameters_in_calls = false +ij_typescript_align_multiline_ternary_operation = false +ij_typescript_align_object_properties = 0 +ij_typescript_align_union_types = false +ij_typescript_align_var_statements = 0 +ij_typescript_array_initializer_new_line_after_left_brace = false +ij_typescript_array_initializer_right_brace_on_new_line = false +ij_typescript_array_initializer_wrap = off +ij_typescript_assignment_wrap = off +ij_typescript_binary_operation_sign_on_next_line = false +ij_typescript_binary_operation_wrap = off +ij_typescript_blacklist_imports = rxjs/Rx, node_modules/**, **/node_modules/**, @angular/material, @angular/material/typings/** +ij_typescript_blank_lines_after_imports = 1 +ij_typescript_blank_lines_around_class = 1 +ij_typescript_blank_lines_around_field = 0 +ij_typescript_blank_lines_around_field_in_interface = 0 +ij_typescript_blank_lines_around_function = 1 +ij_typescript_blank_lines_around_method = 1 +ij_typescript_blank_lines_around_method_in_interface = 1 +ij_typescript_block_brace_style = end_of_line +ij_typescript_block_comment_add_space = false +ij_typescript_block_comment_at_first_column = true +ij_typescript_call_parameters_new_line_after_left_paren = false +ij_typescript_call_parameters_right_paren_on_new_line = false +ij_typescript_call_parameters_wrap = off +ij_typescript_catch_on_new_line = false +ij_typescript_chained_call_dot_on_new_line = true +ij_typescript_class_brace_style = end_of_line +ij_typescript_class_decorator_wrap = split_into_lines +ij_typescript_class_field_decorator_wrap = off +ij_typescript_class_method_decorator_wrap = off +ij_typescript_comma_on_new_line = false +ij_typescript_do_while_brace_force = never +ij_typescript_else_on_new_line = false +ij_typescript_enforce_trailing_comma = keep +ij_typescript_enum_constants_wrap = on_every_item +ij_typescript_extends_keyword_wrap = off +ij_typescript_extends_list_wrap = off +ij_typescript_field_prefix = _ +ij_typescript_file_name_style = relaxed +ij_typescript_finally_on_new_line = false +ij_typescript_for_brace_force = never +ij_typescript_for_statement_new_line_after_left_paren = false +ij_typescript_for_statement_right_paren_on_new_line = false +ij_typescript_for_statement_wrap = off +ij_typescript_force_quote_style = false +ij_typescript_force_semicolon_style = false +ij_typescript_function_expression_brace_style = end_of_line +ij_typescript_function_parameter_decorator_wrap = off +ij_typescript_if_brace_force = never +ij_typescript_import_merge_members = global +ij_typescript_import_prefer_absolute_path = global +ij_typescript_import_sort_members = true +ij_typescript_import_sort_module_name = false +ij_typescript_import_use_node_resolution = true +ij_typescript_imports_wrap = on_every_item +ij_typescript_indent_case_from_switch = true +ij_typescript_indent_chained_calls = true +ij_typescript_indent_package_children = 0 +ij_typescript_jsdoc_include_types = false +ij_typescript_jsx_attribute_value = braces +ij_typescript_keep_blank_lines_in_code = 2 +ij_typescript_keep_first_column_comment = true +ij_typescript_keep_indents_on_empty_lines = false +ij_typescript_keep_line_breaks = true +ij_typescript_keep_simple_blocks_in_one_line = false +ij_typescript_keep_simple_methods_in_one_line = false +ij_typescript_line_comment_add_space = true +ij_typescript_line_comment_at_first_column = false +ij_typescript_method_brace_style = end_of_line +ij_typescript_method_call_chain_wrap = off +ij_typescript_method_parameters_new_line_after_left_paren = false +ij_typescript_method_parameters_right_paren_on_new_line = false +ij_typescript_method_parameters_wrap = off +ij_typescript_object_literal_wrap = on_every_item +ij_typescript_object_types_wrap = on_every_item +ij_typescript_parentheses_expression_new_line_after_left_paren = false +ij_typescript_parentheses_expression_right_paren_on_new_line = false +ij_typescript_place_assignment_sign_on_next_line = false +ij_typescript_prefer_as_type_cast = false +ij_typescript_prefer_explicit_types_function_expression_returns = false +ij_typescript_prefer_explicit_types_function_returns = false +ij_typescript_prefer_explicit_types_vars_fields = false +ij_typescript_prefer_parameters_wrap = false +ij_typescript_property_prefix = +ij_typescript_reformat_c_style_comments = false +ij_typescript_space_after_colon = true +ij_typescript_space_after_comma = true +ij_typescript_space_after_dots_in_rest_parameter = false +ij_typescript_space_after_generator_mult = true +ij_typescript_space_after_property_colon = true +ij_typescript_space_after_quest = true +ij_typescript_space_after_type_colon = true +ij_typescript_space_after_unary_not = false +ij_typescript_space_before_async_arrow_lparen = true +ij_typescript_space_before_catch_keyword = true +ij_typescript_space_before_catch_left_brace = true +ij_typescript_space_before_catch_parentheses = true +ij_typescript_space_before_class_lbrace = true +ij_typescript_space_before_class_left_brace = true +ij_typescript_space_before_colon = true +ij_typescript_space_before_comma = false +ij_typescript_space_before_do_left_brace = true +ij_typescript_space_before_else_keyword = true +ij_typescript_space_before_else_left_brace = true +ij_typescript_space_before_finally_keyword = true +ij_typescript_space_before_finally_left_brace = true +ij_typescript_space_before_for_left_brace = true +ij_typescript_space_before_for_parentheses = true +ij_typescript_space_before_for_semicolon = false +ij_typescript_space_before_function_left_parenth = true +ij_typescript_space_before_generator_mult = false +ij_typescript_space_before_if_left_brace = true +ij_typescript_space_before_if_parentheses = true +ij_typescript_space_before_method_call_parentheses = false +ij_typescript_space_before_method_left_brace = true +ij_typescript_space_before_method_parentheses = false +ij_typescript_space_before_property_colon = false +ij_typescript_space_before_quest = true +ij_typescript_space_before_switch_left_brace = true +ij_typescript_space_before_switch_parentheses = true +ij_typescript_space_before_try_left_brace = true +ij_typescript_space_before_type_colon = false +ij_typescript_space_before_unary_not = false +ij_typescript_space_before_while_keyword = true +ij_typescript_space_before_while_left_brace = true +ij_typescript_space_before_while_parentheses = true +ij_typescript_spaces_around_additive_operators = true +ij_typescript_spaces_around_arrow_function_operator = true +ij_typescript_spaces_around_assignment_operators = true +ij_typescript_spaces_around_bitwise_operators = true +ij_typescript_spaces_around_equality_operators = true +ij_typescript_spaces_around_logical_operators = true +ij_typescript_spaces_around_multiplicative_operators = true +ij_typescript_spaces_around_relational_operators = true +ij_typescript_spaces_around_shift_operators = true +ij_typescript_spaces_around_unary_operator = false +ij_typescript_spaces_within_array_initializer_brackets = false +ij_typescript_spaces_within_brackets = false +ij_typescript_spaces_within_catch_parentheses = false +ij_typescript_spaces_within_for_parentheses = false +ij_typescript_spaces_within_if_parentheses = false +ij_typescript_spaces_within_imports = false +ij_typescript_spaces_within_interpolation_expressions = false +ij_typescript_spaces_within_method_call_parentheses = false +ij_typescript_spaces_within_method_parentheses = false +ij_typescript_spaces_within_object_literal_braces = false +ij_typescript_spaces_within_object_type_braces = true +ij_typescript_spaces_within_parentheses = false +ij_typescript_spaces_within_switch_parentheses = false +ij_typescript_spaces_within_type_assertion = false +ij_typescript_spaces_within_union_types = true +ij_typescript_spaces_within_while_parentheses = false +ij_typescript_special_else_if_treatment = true +ij_typescript_ternary_operation_signs_on_next_line = false +ij_typescript_ternary_operation_wrap = off +ij_typescript_union_types_wrap = on_every_item +ij_typescript_use_chained_calls_group_indents = false +ij_typescript_use_double_quotes = true +ij_typescript_use_explicit_js_extension = auto +ij_typescript_use_import_type = auto +ij_typescript_use_path_mapping = always +ij_typescript_use_public_modifier = false +ij_typescript_use_semicolon_after_statement = true +ij_typescript_var_declaration_wrap = normal +ij_typescript_while_brace_force = never +ij_typescript_while_on_new_line = false +ij_typescript_wrap_comments = false + +[{*.bash,*.sh,*.zsh}] +indent_size = 2 +tab_width = 2 +ij_shell_binary_ops_start_line = false +ij_shell_keep_column_alignment_padding = false +ij_shell_minify_program = false +ij_shell_redirect_followed_by_space = false +ij_shell_switch_cases_indented = false +ij_shell_use_unix_line_separator = true + +[{*.cjs,*.es6,*.js,*.mjs}] +ij_continuation_indent_size = 4 +ij_javascript_align_imports = false +ij_javascript_align_multiline_array_initializer_expression = false +ij_javascript_align_multiline_binary_operation = false +ij_javascript_align_multiline_chained_methods = false +ij_javascript_align_multiline_extends_list = false +ij_javascript_align_multiline_for = true +ij_javascript_align_multiline_parameters = true +ij_javascript_align_multiline_parameters_in_calls = false +ij_javascript_align_multiline_ternary_operation = false +ij_javascript_align_object_properties = 0 +ij_javascript_align_union_types = false +ij_javascript_align_var_statements = 0 +ij_javascript_array_initializer_new_line_after_left_brace = false +ij_javascript_array_initializer_right_brace_on_new_line = false +ij_javascript_array_initializer_wrap = off +ij_javascript_assignment_wrap = off +ij_javascript_binary_operation_sign_on_next_line = false +ij_javascript_binary_operation_wrap = off +ij_javascript_blacklist_imports = rxjs/Rx, node_modules/**, **/node_modules/**, @angular/material, @angular/material/typings/** +ij_javascript_blank_lines_after_imports = 1 +ij_javascript_blank_lines_around_class = 1 +ij_javascript_blank_lines_around_field = 0 +ij_javascript_blank_lines_around_function = 1 +ij_javascript_blank_lines_around_method = 1 +ij_javascript_block_brace_style = end_of_line +ij_javascript_block_comment_add_space = false +ij_javascript_block_comment_at_first_column = true +ij_javascript_call_parameters_new_line_after_left_paren = false +ij_javascript_call_parameters_right_paren_on_new_line = false +ij_javascript_call_parameters_wrap = off +ij_javascript_catch_on_new_line = false +ij_javascript_chained_call_dot_on_new_line = true +ij_javascript_class_brace_style = end_of_line +ij_javascript_class_decorator_wrap = split_into_lines +ij_javascript_class_field_decorator_wrap = off +ij_javascript_class_method_decorator_wrap = off +ij_javascript_comma_on_new_line = false +ij_javascript_do_while_brace_force = never +ij_javascript_else_on_new_line = false +ij_javascript_enforce_trailing_comma = keep +ij_javascript_extends_keyword_wrap = off +ij_javascript_extends_list_wrap = off +ij_javascript_field_prefix = _ +ij_javascript_file_name_style = relaxed +ij_javascript_finally_on_new_line = false +ij_javascript_for_brace_force = never +ij_javascript_for_statement_new_line_after_left_paren = false +ij_javascript_for_statement_right_paren_on_new_line = false +ij_javascript_for_statement_wrap = off +ij_javascript_force_quote_style = false +ij_javascript_force_semicolon_style = false +ij_javascript_function_expression_brace_style = end_of_line +ij_javascript_function_parameter_decorator_wrap = off +ij_javascript_if_brace_force = never +ij_javascript_import_merge_members = global +ij_javascript_import_prefer_absolute_path = global +ij_javascript_import_sort_members = true +ij_javascript_import_sort_module_name = false +ij_javascript_import_use_node_resolution = true +ij_javascript_imports_wrap = on_every_item +ij_javascript_indent_case_from_switch = true +ij_javascript_indent_chained_calls = true +ij_javascript_indent_package_children = 0 +ij_javascript_jsx_attribute_value = braces +ij_javascript_keep_blank_lines_in_code = 2 +ij_javascript_keep_first_column_comment = true +ij_javascript_keep_indents_on_empty_lines = false +ij_javascript_keep_line_breaks = true +ij_javascript_keep_simple_blocks_in_one_line = false +ij_javascript_keep_simple_methods_in_one_line = false +ij_javascript_line_comment_add_space = true +ij_javascript_line_comment_at_first_column = false +ij_javascript_method_brace_style = end_of_line +ij_javascript_method_call_chain_wrap = off +ij_javascript_method_parameters_new_line_after_left_paren = false +ij_javascript_method_parameters_right_paren_on_new_line = false +ij_javascript_method_parameters_wrap = off +ij_javascript_object_literal_wrap = on_every_item +ij_javascript_object_types_wrap = on_every_item +ij_javascript_parentheses_expression_new_line_after_left_paren = false +ij_javascript_parentheses_expression_right_paren_on_new_line = false +ij_javascript_place_assignment_sign_on_next_line = false +ij_javascript_prefer_as_type_cast = false +ij_javascript_prefer_explicit_types_function_expression_returns = false +ij_javascript_prefer_explicit_types_function_returns = false +ij_javascript_prefer_explicit_types_vars_fields = false +ij_javascript_prefer_parameters_wrap = false +ij_javascript_property_prefix = +ij_javascript_reformat_c_style_comments = false +ij_javascript_space_after_colon = true +ij_javascript_space_after_comma = true +ij_javascript_space_after_dots_in_rest_parameter = false +ij_javascript_space_after_generator_mult = true +ij_javascript_space_after_property_colon = true +ij_javascript_space_after_quest = true +ij_javascript_space_after_type_colon = true +ij_javascript_space_after_unary_not = false +ij_javascript_space_before_async_arrow_lparen = true +ij_javascript_space_before_catch_keyword = true +ij_javascript_space_before_catch_left_brace = true +ij_javascript_space_before_catch_parentheses = true +ij_javascript_space_before_class_lbrace = true +ij_javascript_space_before_class_left_brace = true +ij_javascript_space_before_colon = true +ij_javascript_space_before_comma = false +ij_javascript_space_before_do_left_brace = true +ij_javascript_space_before_else_keyword = true +ij_javascript_space_before_else_left_brace = true +ij_javascript_space_before_finally_keyword = true +ij_javascript_space_before_finally_left_brace = true +ij_javascript_space_before_for_left_brace = true +ij_javascript_space_before_for_parentheses = true +ij_javascript_space_before_for_semicolon = false +ij_javascript_space_before_function_left_parenth = true +ij_javascript_space_before_generator_mult = false +ij_javascript_space_before_if_left_brace = true +ij_javascript_space_before_if_parentheses = true +ij_javascript_space_before_method_call_parentheses = false +ij_javascript_space_before_method_left_brace = true +ij_javascript_space_before_method_parentheses = false +ij_javascript_space_before_property_colon = false +ij_javascript_space_before_quest = true +ij_javascript_space_before_switch_left_brace = true +ij_javascript_space_before_switch_parentheses = true +ij_javascript_space_before_try_left_brace = true +ij_javascript_space_before_type_colon = false +ij_javascript_space_before_unary_not = false +ij_javascript_space_before_while_keyword = true +ij_javascript_space_before_while_left_brace = true +ij_javascript_space_before_while_parentheses = true +ij_javascript_spaces_around_additive_operators = true +ij_javascript_spaces_around_arrow_function_operator = true +ij_javascript_spaces_around_assignment_operators = true +ij_javascript_spaces_around_bitwise_operators = true +ij_javascript_spaces_around_equality_operators = true +ij_javascript_spaces_around_logical_operators = true +ij_javascript_spaces_around_multiplicative_operators = true +ij_javascript_spaces_around_relational_operators = true +ij_javascript_spaces_around_shift_operators = true +ij_javascript_spaces_around_unary_operator = false +ij_javascript_spaces_within_array_initializer_brackets = false +ij_javascript_spaces_within_brackets = false +ij_javascript_spaces_within_catch_parentheses = false +ij_javascript_spaces_within_for_parentheses = false +ij_javascript_spaces_within_if_parentheses = false +ij_javascript_spaces_within_imports = false +ij_javascript_spaces_within_interpolation_expressions = false +ij_javascript_spaces_within_method_call_parentheses = false +ij_javascript_spaces_within_method_parentheses = false +ij_javascript_spaces_within_object_literal_braces = false +ij_javascript_spaces_within_object_type_braces = true +ij_javascript_spaces_within_parentheses = false +ij_javascript_spaces_within_switch_parentheses = false +ij_javascript_spaces_within_type_assertion = false +ij_javascript_spaces_within_union_types = true +ij_javascript_spaces_within_while_parentheses = false +ij_javascript_special_else_if_treatment = true +ij_javascript_ternary_operation_signs_on_next_line = false +ij_javascript_ternary_operation_wrap = off +ij_javascript_union_types_wrap = on_every_item +ij_javascript_use_chained_calls_group_indents = false +ij_javascript_use_double_quotes = true +ij_javascript_use_explicit_js_extension = auto +ij_javascript_use_import_type = auto +ij_javascript_use_path_mapping = always +ij_javascript_use_public_modifier = false +ij_javascript_use_semicolon_after_statement = true +ij_javascript_var_declaration_wrap = normal +ij_javascript_while_brace_force = never +ij_javascript_while_on_new_line = false +ij_javascript_wrap_comments = false + +[{*.cjsx,*.coffee}] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 2 +ij_coffeescript_align_function_body = false +ij_coffeescript_align_imports = false +ij_coffeescript_align_multiline_array_initializer_expression = true +ij_coffeescript_align_multiline_parameters = true +ij_coffeescript_align_multiline_parameters_in_calls = false +ij_coffeescript_align_object_properties = 0 +ij_coffeescript_align_union_types = false +ij_coffeescript_align_var_statements = 0 +ij_coffeescript_array_initializer_new_line_after_left_brace = false +ij_coffeescript_array_initializer_right_brace_on_new_line = false +ij_coffeescript_array_initializer_wrap = normal +ij_coffeescript_blacklist_imports = rxjs/Rx, node_modules/**, **/node_modules/**, @angular/material, @angular/material/typings/** +ij_coffeescript_blank_lines_around_function = 1 +ij_coffeescript_call_parameters_new_line_after_left_paren = false +ij_coffeescript_call_parameters_right_paren_on_new_line = false +ij_coffeescript_call_parameters_wrap = normal +ij_coffeescript_chained_call_dot_on_new_line = true +ij_coffeescript_class_decorator_wrap = split_into_lines +ij_coffeescript_class_field_decorator_wrap = off +ij_coffeescript_class_method_decorator_wrap = off +ij_coffeescript_comma_on_new_line = false +ij_coffeescript_enforce_trailing_comma = keep +ij_coffeescript_field_prefix = _ +ij_coffeescript_file_name_style = relaxed +ij_coffeescript_force_quote_style = false +ij_coffeescript_force_semicolon_style = false +ij_coffeescript_function_expression_brace_style = end_of_line +ij_coffeescript_function_parameter_decorator_wrap = off +ij_coffeescript_import_merge_members = global +ij_coffeescript_import_prefer_absolute_path = global +ij_coffeescript_import_sort_members = true +ij_coffeescript_import_sort_module_name = false +ij_coffeescript_import_use_node_resolution = true +ij_coffeescript_imports_wrap = on_every_item +ij_coffeescript_indent_chained_calls = true +ij_coffeescript_indent_package_children = 0 +ij_coffeescript_jsx_attribute_value = braces +ij_coffeescript_keep_blank_lines_in_code = 2 +ij_coffeescript_keep_first_column_comment = true +ij_coffeescript_keep_indents_on_empty_lines = false +ij_coffeescript_keep_line_breaks = true +ij_coffeescript_keep_simple_methods_in_one_line = false +ij_coffeescript_method_parameters_new_line_after_left_paren = false +ij_coffeescript_method_parameters_right_paren_on_new_line = false +ij_coffeescript_method_parameters_wrap = off +ij_coffeescript_object_literal_wrap = on_every_item +ij_coffeescript_object_types_wrap = on_every_item +ij_coffeescript_prefer_as_type_cast = false +ij_coffeescript_prefer_explicit_types_function_expression_returns = false +ij_coffeescript_prefer_explicit_types_function_returns = false +ij_coffeescript_prefer_explicit_types_vars_fields = false +ij_coffeescript_property_prefix = +ij_coffeescript_reformat_c_style_comments = false +ij_coffeescript_space_after_comma = true +ij_coffeescript_space_after_dots_in_rest_parameter = false +ij_coffeescript_space_after_generator_mult = true +ij_coffeescript_space_after_property_colon = true +ij_coffeescript_space_after_type_colon = true +ij_coffeescript_space_after_unary_not = false +ij_coffeescript_space_before_async_arrow_lparen = true +ij_coffeescript_space_before_class_lbrace = true +ij_coffeescript_space_before_comma = false +ij_coffeescript_space_before_function_left_parenth = true +ij_coffeescript_space_before_generator_mult = false +ij_coffeescript_space_before_property_colon = false +ij_coffeescript_space_before_type_colon = false +ij_coffeescript_space_before_unary_not = false +ij_coffeescript_spaces_around_additive_operators = true +ij_coffeescript_spaces_around_arrow_function_operator = true +ij_coffeescript_spaces_around_assignment_operators = true +ij_coffeescript_spaces_around_bitwise_operators = true +ij_coffeescript_spaces_around_equality_operators = true +ij_coffeescript_spaces_around_logical_operators = true +ij_coffeescript_spaces_around_multiplicative_operators = true +ij_coffeescript_spaces_around_relational_operators = true +ij_coffeescript_spaces_around_shift_operators = true +ij_coffeescript_spaces_around_unary_operator = false +ij_coffeescript_spaces_within_array_initializer_braces = false +ij_coffeescript_spaces_within_array_initializer_brackets = false +ij_coffeescript_spaces_within_imports = false +ij_coffeescript_spaces_within_index_brackets = false +ij_coffeescript_spaces_within_interpolation_expressions = false +ij_coffeescript_spaces_within_method_call_parentheses = false +ij_coffeescript_spaces_within_method_parentheses = false +ij_coffeescript_spaces_within_object_braces = false +ij_coffeescript_spaces_within_object_literal_braces = false +ij_coffeescript_spaces_within_object_type_braces = true +ij_coffeescript_spaces_within_range_brackets = false +ij_coffeescript_spaces_within_type_assertion = false +ij_coffeescript_spaces_within_union_types = true +ij_coffeescript_union_types_wrap = on_every_item +ij_coffeescript_use_chained_calls_group_indents = false +ij_coffeescript_use_double_quotes = true +ij_coffeescript_use_explicit_js_extension = auto +ij_coffeescript_use_import_type = auto +ij_coffeescript_use_path_mapping = always +ij_coffeescript_use_public_modifier = false +ij_coffeescript_use_semicolon_after_statement = false +ij_coffeescript_var_declaration_wrap = normal + +[{*.ft,*.vm,*.vsl}] +ij_vtl_keep_indents_on_empty_lines = false + +[{*.gant,*.groovy,*.gson,*.gy}] +ij_groovy_align_group_field_declarations = false +ij_groovy_align_multiline_array_initializer_expression = false +ij_groovy_align_multiline_assignment = false +ij_groovy_align_multiline_binary_operation = false +ij_groovy_align_multiline_chained_methods = false +ij_groovy_align_multiline_extends_list = false +ij_groovy_align_multiline_for = true +ij_groovy_align_multiline_list_or_map = true +ij_groovy_align_multiline_method_parentheses = false +ij_groovy_align_multiline_parameters = true +ij_groovy_align_multiline_parameters_in_calls = false +ij_groovy_align_multiline_resources = true +ij_groovy_align_multiline_ternary_operation = false +ij_groovy_align_multiline_throws_list = false +ij_groovy_align_named_args_in_map = true +ij_groovy_align_throws_keyword = false +ij_groovy_array_initializer_new_line_after_left_brace = false +ij_groovy_array_initializer_right_brace_on_new_line = false +ij_groovy_array_initializer_wrap = off +ij_groovy_assert_statement_wrap = off +ij_groovy_assignment_wrap = off +ij_groovy_binary_operation_wrap = off +ij_groovy_blank_lines_after_class_header = 0 +ij_groovy_blank_lines_after_imports = 1 +ij_groovy_blank_lines_after_package = 1 +ij_groovy_blank_lines_around_class = 1 +ij_groovy_blank_lines_around_field = 0 +ij_groovy_blank_lines_around_field_in_interface = 0 +ij_groovy_blank_lines_around_method = 1 +ij_groovy_blank_lines_around_method_in_interface = 1 +ij_groovy_blank_lines_before_imports = 1 +ij_groovy_blank_lines_before_method_body = 0 +ij_groovy_blank_lines_before_package = 0 +ij_groovy_block_brace_style = end_of_line +ij_groovy_block_comment_add_space = false +ij_groovy_block_comment_at_first_column = true +ij_groovy_call_parameters_new_line_after_left_paren = false +ij_groovy_call_parameters_right_paren_on_new_line = false +ij_groovy_call_parameters_wrap = off +ij_groovy_catch_on_new_line = false +ij_groovy_class_annotation_wrap = split_into_lines +ij_groovy_class_brace_style = end_of_line +ij_groovy_class_count_to_use_import_on_demand = 999 +ij_groovy_do_while_brace_force = never +ij_groovy_else_on_new_line = false +ij_groovy_enable_groovydoc_formatting = true +ij_groovy_enum_constants_wrap = off +ij_groovy_extends_keyword_wrap = off +ij_groovy_extends_list_wrap = off +ij_groovy_field_annotation_wrap = split_into_lines +ij_groovy_finally_on_new_line = false +ij_groovy_for_brace_force = never +ij_groovy_for_statement_new_line_after_left_paren = false +ij_groovy_for_statement_right_paren_on_new_line = false +ij_groovy_for_statement_wrap = off +ij_groovy_ginq_general_clause_wrap_policy = 2 +ij_groovy_ginq_having_wrap_policy = 1 +ij_groovy_ginq_indent_having_clause = true +ij_groovy_ginq_indent_on_clause = true +ij_groovy_ginq_on_wrap_policy = 1 +ij_groovy_ginq_space_after_keyword = true +ij_groovy_if_brace_force = never +ij_groovy_import_annotation_wrap = 2 +ij_groovy_imports_layout = java.**, |, javax.**, |, groovy.**, org.apache.groovy.**, org.codehaus.groovy.**, |, jakarta.**, |, *, |, io.spring.**, org.springframework.**, |, grails.**, org.apache.grails.**, org.grails.**, |, $* +ij_groovy_indent_case_from_switch = true +ij_groovy_indent_label_blocks = true +ij_groovy_insert_inner_class_imports = false +ij_groovy_keep_blank_lines_before_right_brace = 2 +ij_groovy_keep_blank_lines_in_code = 2 +ij_groovy_keep_blank_lines_in_declarations = 2 +ij_groovy_keep_control_statement_in_one_line = true +ij_groovy_keep_first_column_comment = true +ij_groovy_keep_indents_on_empty_lines = false +ij_groovy_keep_line_breaks = true +ij_groovy_keep_multiple_expressions_in_one_line = false +ij_groovy_keep_simple_blocks_in_one_line = false +ij_groovy_keep_simple_classes_in_one_line = true +ij_groovy_keep_simple_lambdas_in_one_line = true +ij_groovy_keep_simple_methods_in_one_line = true +ij_groovy_label_indent_absolute = false +ij_groovy_label_indent_size = 0 +ij_groovy_lambda_brace_style = end_of_line +ij_groovy_layout_static_imports_separately = true +ij_groovy_line_comment_add_space = false +ij_groovy_line_comment_add_space_on_reformat = false +ij_groovy_line_comment_at_first_column = true +ij_groovy_method_annotation_wrap = split_into_lines +ij_groovy_method_brace_style = end_of_line +ij_groovy_method_call_chain_wrap = off +ij_groovy_method_parameters_new_line_after_left_paren = false +ij_groovy_method_parameters_right_paren_on_new_line = false +ij_groovy_method_parameters_wrap = off +ij_groovy_modifier_list_wrap = false +ij_groovy_names_count_to_use_import_on_demand = 999 +ij_groovy_packages_to_use_import_on_demand = +ij_groovy_parameter_annotation_wrap = off +ij_groovy_parentheses_expression_new_line_after_left_paren = false +ij_groovy_parentheses_expression_right_paren_on_new_line = false +ij_groovy_prefer_parameters_wrap = false +ij_groovy_resource_list_new_line_after_left_paren = false +ij_groovy_resource_list_right_paren_on_new_line = false +ij_groovy_resource_list_wrap = off +ij_groovy_space_after_assert_separator = true +ij_groovy_space_after_colon = true +ij_groovy_space_after_comma = true +ij_groovy_space_after_comma_in_type_arguments = true +ij_groovy_space_after_for_semicolon = true +ij_groovy_space_after_quest = true +ij_groovy_space_after_type_cast = true +ij_groovy_space_before_annotation_parameter_list = false +ij_groovy_space_before_array_initializer_left_brace = false +ij_groovy_space_before_assert_separator = false +ij_groovy_space_before_catch_keyword = true +ij_groovy_space_before_catch_left_brace = true +ij_groovy_space_before_catch_parentheses = true +ij_groovy_space_before_class_left_brace = true +ij_groovy_space_before_closure_left_brace = true +ij_groovy_space_before_colon = true +ij_groovy_space_before_comma = false +ij_groovy_space_before_do_left_brace = true +ij_groovy_space_before_else_keyword = true +ij_groovy_space_before_else_left_brace = true +ij_groovy_space_before_finally_keyword = true +ij_groovy_space_before_finally_left_brace = true +ij_groovy_space_before_for_left_brace = true +ij_groovy_space_before_for_parentheses = true +ij_groovy_space_before_for_semicolon = false +ij_groovy_space_before_if_left_brace = true +ij_groovy_space_before_if_parentheses = true +ij_groovy_space_before_method_call_parentheses = false +ij_groovy_space_before_method_left_brace = true +ij_groovy_space_before_method_parentheses = false +ij_groovy_space_before_quest = true +ij_groovy_space_before_record_parentheses = false +ij_groovy_space_before_switch_left_brace = true +ij_groovy_space_before_switch_parentheses = true +ij_groovy_space_before_synchronized_left_brace = true +ij_groovy_space_before_synchronized_parentheses = true +ij_groovy_space_before_try_left_brace = true +ij_groovy_space_before_try_parentheses = true +ij_groovy_space_before_while_keyword = true +ij_groovy_space_before_while_left_brace = true +ij_groovy_space_before_while_parentheses = true +ij_groovy_space_in_named_argument = true +ij_groovy_space_in_named_argument_before_colon = false +ij_groovy_space_within_empty_array_initializer_braces = false +ij_groovy_space_within_empty_method_call_parentheses = false +ij_groovy_spaces_around_additive_operators = true +ij_groovy_spaces_around_assignment_operators = true +ij_groovy_spaces_around_bitwise_operators = true +ij_groovy_spaces_around_equality_operators = true +ij_groovy_spaces_around_lambda_arrow = true +ij_groovy_spaces_around_logical_operators = true +ij_groovy_spaces_around_multiplicative_operators = true +ij_groovy_spaces_around_regex_operators = true +ij_groovy_spaces_around_relational_operators = true +ij_groovy_spaces_around_shift_operators = true +ij_groovy_spaces_within_annotation_parentheses = false +ij_groovy_spaces_within_array_initializer_braces = false +ij_groovy_spaces_within_braces = true +ij_groovy_spaces_within_brackets = false +ij_groovy_spaces_within_cast_parentheses = false +ij_groovy_spaces_within_catch_parentheses = false +ij_groovy_spaces_within_for_parentheses = false +ij_groovy_spaces_within_gstring_injection_braces = false +ij_groovy_spaces_within_if_parentheses = false +ij_groovy_spaces_within_list_or_map = false +ij_groovy_spaces_within_method_call_parentheses = false +ij_groovy_spaces_within_method_parentheses = false +ij_groovy_spaces_within_parentheses = false +ij_groovy_spaces_within_switch_parentheses = false +ij_groovy_spaces_within_synchronized_parentheses = false +ij_groovy_spaces_within_try_parentheses = false +ij_groovy_spaces_within_tuple_expression = false +ij_groovy_spaces_within_while_parentheses = false +ij_groovy_special_else_if_treatment = true +ij_groovy_ternary_operation_wrap = off +ij_groovy_throws_keyword_wrap = off +ij_groovy_throws_list_wrap = off +ij_groovy_use_flying_geese_braces = false +ij_groovy_use_fq_class_names = false +ij_groovy_use_fq_class_names_in_javadoc = true +ij_groovy_use_relative_indents = false +ij_groovy_use_single_class_imports = true +ij_groovy_variable_annotation_wrap = off +ij_groovy_while_brace_force = never +ij_groovy_while_on_new_line = false +ij_groovy_wrap_chain_calls_after_dot = false +ij_groovy_wrap_long_lines = false + +[{*.har,*.jsb2,*.jsb3,*.json,*.jsonc,*.postman_collection,*.postman_collection.json,*.postman_environment,*.postman_environment.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,.ws-context,jest.config}] +indent_size = 2 +ij_json_array_wrapping = split_into_lines +ij_json_keep_blank_lines_in_code = 0 +ij_json_keep_indents_on_empty_lines = false +ij_json_keep_line_breaks = true +ij_json_keep_trailing_comma = false +ij_json_object_wrapping = split_into_lines +ij_json_property_alignment = do_not_align +ij_json_space_after_colon = true +ij_json_space_after_comma = true +ij_json_space_before_colon = false +ij_json_space_before_comma = false +ij_json_spaces_within_braces = false +ij_json_spaces_within_brackets = false +ij_json_wrap_long_lines = false + +[{*.hcl,*.nomad}] +indent_size = 2 +ij_hcl_keep_blank_lines_in_code = 2 +ij_hcl_keep_indents_on_empty_lines = false +ij_hcl_keep_line_breaks = true +ij_hcl_space_after_comma = true +ij_hcl_space_before_comma = false +ij_hcl_spaces_around_assignment_operators = true +ij_hcl_spaces_within_braces = false +ij_hcl_spaces_within_brackets = false +ij_hcl_wrap_long_lines = false + +[{*.htm,*.html,*.sht,*.shtm,*.shtml}] +ij_html_add_new_line_before_tags = body, div, p, form, h1, h2, h3 +ij_html_align_attributes = true +ij_html_align_text = false +ij_html_attribute_wrap = normal +ij_html_block_comment_add_space = false +ij_html_block_comment_at_first_column = true +ij_html_do_not_align_children_of_min_lines = 0 +ij_html_do_not_break_if_inline_tags = title, h1, h2, h3, h4, h5, h6, p +ij_html_do_not_indent_children_of_tags = html, body, thead, tbody, tfoot +ij_html_enforce_quotes = false +ij_html_inline_tags = a, abbr, acronym, b, basefont, bdo, big, br, cite, cite, code, dfn, em, font, i, img, input, kbd, label, q, s, samp, select, small, span, strike, strong, sub, sup, textarea, tt, u, var +ij_html_keep_blank_lines = 2 +ij_html_keep_indents_on_empty_lines = false +ij_html_keep_line_breaks = true +ij_html_keep_line_breaks_in_text = true +ij_html_keep_whitespaces = false +ij_html_keep_whitespaces_inside = span, pre, textarea +ij_html_line_comment_at_first_column = true +ij_html_new_line_after_last_attribute = never +ij_html_new_line_before_first_attribute = never +ij_html_quote_style = double +ij_html_remove_new_line_before_tags = br +ij_html_space_after_tag_name = false +ij_html_space_around_equality_in_attribute = false +ij_html_space_inside_empty_tag = false +ij_html_text_wrap = normal + +[{*.http,*.rest}] +ij_continuation_indent_size = 4 +ij_http-request_call_parameters_wrap = normal +ij_http-request_method_parameters_wrap = split_into_lines +ij_http-request_space_before_comma = true +ij_http-request_spaces_around_assignment_operators = true + +[{*.jsf,*.jsp,*.jspf,*.tag,*.tagf,*.xjsp}] +ij_jsp_jsp_prefer_comma_separated_import_list = false +ij_jsp_keep_indents_on_empty_lines = false + +[{*.jspx,*.tagx}] +ij_jspx_keep_indents_on_empty_lines = false + +[{*.kt,*.kts}] +ij_kotlin_align_in_columns_case_branch = false +ij_kotlin_align_multiline_binary_operation = false +ij_kotlin_align_multiline_extends_list = false +ij_kotlin_align_multiline_method_parentheses = false +ij_kotlin_align_multiline_parameters = true +ij_kotlin_align_multiline_parameters_in_calls = false +ij_kotlin_allow_trailing_comma = false +ij_kotlin_allow_trailing_comma_collection_literal_expression = false +ij_kotlin_allow_trailing_comma_context_receiver_list = true +ij_kotlin_allow_trailing_comma_destructuring_declaration = true +ij_kotlin_allow_trailing_comma_function_literal = true +ij_kotlin_allow_trailing_comma_indices = false +ij_kotlin_allow_trailing_comma_on_call_site = false +ij_kotlin_allow_trailing_comma_type_argument_list = false +ij_kotlin_allow_trailing_comma_type_parameter_list = true +ij_kotlin_allow_trailing_comma_value_argument_list = false +ij_kotlin_allow_trailing_comma_value_parameter_list = true +ij_kotlin_allow_trailing_comma_when_entry = true +ij_kotlin_assignment_wrap = normal +ij_kotlin_blank_lines_after_class_header = 0 +ij_kotlin_blank_lines_around_block_when_branches = 0 +ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1 +ij_kotlin_block_comment_add_space = false +ij_kotlin_block_comment_at_first_column = true +ij_kotlin_call_parameters_new_line_after_left_paren = true +ij_kotlin_call_parameters_right_paren_on_new_line = true +ij_kotlin_call_parameters_wrap = on_every_item +ij_kotlin_catch_on_new_line = false +ij_kotlin_class_annotation_wrap = split_into_lines +ij_kotlin_continuation_indent_for_chained_calls = false +ij_kotlin_continuation_indent_for_expression_bodies = false +ij_kotlin_continuation_indent_in_argument_lists = false +ij_kotlin_continuation_indent_in_elvis = false +ij_kotlin_continuation_indent_in_if_conditions = false +ij_kotlin_continuation_indent_in_parameter_lists = false +ij_kotlin_continuation_indent_in_supertype_lists = false +ij_kotlin_else_on_new_line = false +ij_kotlin_enum_constants_wrap = off +ij_kotlin_extends_list_wrap = normal +ij_kotlin_field_annotation_wrap = split_into_lines +ij_kotlin_finally_on_new_line = false +ij_kotlin_function_context_parameters_wrap = split_into_lines +ij_kotlin_if_rparen_on_new_line = true +ij_kotlin_import_nested_classes = false +ij_kotlin_imports_layout = *, java.**, javax.**, kotlin.**, ^ +ij_kotlin_indent_before_arrow_on_new_line = true +ij_kotlin_insert_whitespaces_in_simple_one_line_method = true +ij_kotlin_keep_blank_lines_before_right_brace = 2 +ij_kotlin_keep_blank_lines_in_code = 2 +ij_kotlin_keep_blank_lines_in_declarations = 2 +ij_kotlin_keep_first_column_comment = true +ij_kotlin_keep_indents_on_empty_lines = false +ij_kotlin_keep_line_breaks = true +ij_kotlin_lbrace_on_next_line = false +ij_kotlin_line_break_after_multiline_when_entry = true +ij_kotlin_line_comment_add_space = false +ij_kotlin_line_comment_add_space_on_reformat = false +ij_kotlin_line_comment_at_first_column = true +ij_kotlin_method_annotation_wrap = split_into_lines +ij_kotlin_method_call_chain_wrap = normal +ij_kotlin_method_parameters_new_line_after_left_paren = true +ij_kotlin_method_parameters_right_paren_on_new_line = true +ij_kotlin_method_parameters_wrap = on_every_item +ij_kotlin_name_count_to_use_star_import = 5 +ij_kotlin_name_count_to_use_star_import_for_members = 3 +ij_kotlin_packages_to_use_import_on_demand = java.util.*, kotlinx.android.synthetic.**, io.ktor.** +ij_kotlin_parameter_annotation_wrap = off +ij_kotlin_property_context_parameters_wrap = split_into_lines +ij_kotlin_space_after_comma = true +ij_kotlin_space_after_extend_colon = true +ij_kotlin_space_after_type_colon = true +ij_kotlin_space_before_catch_parentheses = true +ij_kotlin_space_before_comma = false +ij_kotlin_space_before_extend_colon = true +ij_kotlin_space_before_for_parentheses = true +ij_kotlin_space_before_if_parentheses = true +ij_kotlin_space_before_lambda_arrow = true +ij_kotlin_space_before_type_colon = false +ij_kotlin_space_before_when_parentheses = true +ij_kotlin_space_before_while_parentheses = true +ij_kotlin_spaces_around_additive_operators = true +ij_kotlin_spaces_around_assignment_operators = true +ij_kotlin_spaces_around_elvis = true +ij_kotlin_spaces_around_equality_operators = true +ij_kotlin_spaces_around_function_type_arrow = true +ij_kotlin_spaces_around_logical_operators = true +ij_kotlin_spaces_around_multiplicative_operators = true +ij_kotlin_spaces_around_range = false +ij_kotlin_spaces_around_relational_operators = true +ij_kotlin_spaces_around_unary_operator = false +ij_kotlin_spaces_around_when_arrow = true +ij_kotlin_variable_annotation_wrap = off +ij_kotlin_while_on_new_line = false +ij_kotlin_wrap_elvis_expressions = 1 +ij_kotlin_wrap_expression_body_functions = 1 +ij_kotlin_wrap_first_method_in_call_chain = false + +[{*.markdown,*.md}] +ij_markdown_force_one_space_after_blockquote_symbol = true +ij_markdown_force_one_space_after_header_symbol = true +ij_markdown_force_one_space_after_list_bullet = true +ij_markdown_force_one_space_between_words = true +ij_markdown_format_tables = true +ij_markdown_insert_quote_arrows_on_wrap = true +ij_markdown_keep_indents_on_empty_lines = false +ij_markdown_keep_line_breaks_inside_text_blocks = true +ij_markdown_max_lines_around_block_elements = 1 +ij_markdown_max_lines_around_header = 1 +ij_markdown_max_lines_between_paragraphs = 1 +ij_markdown_min_lines_around_block_elements = 1 +ij_markdown_min_lines_around_header = 1 +ij_markdown_min_lines_between_paragraphs = 1 +ij_markdown_wrap_text_if_long = true +ij_markdown_wrap_text_inside_blockquotes = true + +[{*.pb,*.textproto,*.txtpb}] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 4 +ij_prototext_keep_blank_lines_in_code = 2 +ij_prototext_keep_indents_on_empty_lines = false +ij_prototext_keep_line_breaks = true +ij_prototext_space_after_colon = true +ij_prototext_space_after_comma = true +ij_prototext_space_before_colon = false +ij_prototext_space_before_comma = false +ij_prototext_spaces_within_braces = true +ij_prototext_spaces_within_brackets = false + +[{*.properties,spring.handlers,spring.schemas}] +ij_properties_align_group_field_declarations = false +ij_properties_keep_blank_lines = false +ij_properties_key_value_delimiter = equals +ij_properties_spaces_around_key_value_delimiter = false + +[{*.qute.htm,*.qute.html,*.qute.json,*.qute.txt,*.qute.yaml,*.qute.yml}] +ij_qute_keep_indents_on_empty_lines = false + +[{*.tf,*.tfvars}] +indent_size = 2 +ij_hcl-terraform_array_wrapping = normal +ij_hcl-terraform_import_providers_automatically = true +ij_hcl-terraform_keep_blank_lines_in_code = 2 +ij_hcl-terraform_keep_indents_on_empty_lines = false +ij_hcl-terraform_keep_line_breaks = true +ij_hcl-terraform_line_commenter_character = pound_sign_( #) +ij_hcl-terraform_object_wrapping = normal +ij_hcl-terraform_property_alignment = on_equals +ij_hcl-terraform_run_tf_fmt_on_reformat = true +ij_hcl-terraform_space_after_comma = true +ij_hcl-terraform_space_before_comma = false +ij_hcl-terraform_spaces_around_assignment_operators = true +ij_hcl-terraform_spaces_within_braces = false +ij_hcl-terraform_spaces_within_brackets = false +ij_hcl-terraform_wrap_long_lines = false + +[{*.toml,Cargo.lock,Cargo.toml.orig,Gopkg.lock,Pipfile,pdm.lock,poetry.lock,uv.lock}] +ij_toml_keep_indents_on_empty_lines = false + +[{*.yaml,*.yml}] +indent_size = 2 +ij_yaml_align_values_properties = do_not_align +ij_yaml_autoinsert_sequence_marker = true +ij_yaml_block_mapping_on_new_line = false +ij_yaml_indent_sequence_value = true +ij_yaml_keep_indents_on_empty_lines = false +ij_yaml_keep_line_breaks = true +ij_yaml_line_comment_add_space = false +ij_yaml_line_comment_add_space_on_reformat = false +ij_yaml_line_comment_at_first_column = true +ij_yaml_sequence_on_new_line = false +ij_yaml_space_before_colon = false +ij_yaml_spaces_within_braces = true +ij_yaml_spaces_within_brackets = true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 2e1c0dc..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,28 +0,0 @@ -# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time -# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle - -name: Build - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - cache: gradle - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - name: Build with Gradle - run: ./gradlew build diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e689a8f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,110 @@ +name: "CI" +on: + push: + branches: + - '[0-9]+.[0-9]+.x' + - 'main' + pull_request: + workflow_dispatch: +env: + JAVA_DISTRIBUTION: 'liberica' +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} +jobs: + build: + name: "Build & Test" + runs-on: ubuntu-24.04 + steps: + - name: "Output Agent IP" # in the event your agent has network issues, you can use this to debug + run: curl -s https://api.ipify.org + - name: "📥 Checkout repository" + uses: actions/checkout@v6 + - name: "Export .sdkmanrc properties" + uses: apache/grails-github-actions/export-gradle-properties@asf + with: + file: ".sdkmanrc" + prefix: "SDKMANRC_" + - name: "Determine Java Version" + run: echo "SDKMANRC_java=${{ env.SDKMANRC_java }}" | sed 's/-.*//' >> $GITHUB_ENV + - name: "☕️ Setup JDK" + uses: actions/setup-java@v5 + with: + distribution: ${{ env.JAVA_DISTRIBUTION }} + java-version: ${{ env.SDKMANRC_java }} + - name: 'Ensure Common Build Date' # to ensure a reproducible build + run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> "$GITHUB_ENV" + - name: "Export gradle.properties properties" + uses: apache/grails-github-actions/export-gradle-properties@asf + - name: "🐘 Setup Gradle" + uses: gradle/actions/setup-gradle@v5 + with: + build-scan-publish: ${{ env.ciBuildScanPublish }} + build-scan-terms-of-use-url: ${{ env.ciBuildScanTermsOfUseUrl }} + build-scan-terms-of-use-agree: ${{ env.ciBuildScanTermsOfUseAgree }} + - name: "🔨 Build project without tests" + if: ${{ contains(github.event.head_commit.message, '[skip tests]') }} + run: > + ./gradlew build + --continue + --stacktrace + -PskipTests + -PskipCodeStyle + - name: "🔨 Build project with tests" + if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }} + run: > + ./gradlew build + --continue + --stacktrace + --rerun-tasks + -PskipCodeStyle + publish: + # only run the publishing task on this repo (not on forks) + if: github.repository_owner == 'gpc' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') + needs: build + name: "Publish Snapshot" + runs-on: ubuntu-24.04 + steps: + - name: "Output Agent IP" # in the event your agent has network issues, you can use this to debug + run: curl -s https://api.ipify.org + - name: "📥 Checkout repository" + uses: actions/checkout@v6 + - name: "Export .sdkmanrc properties" + uses: apache/grails-github-actions/export-gradle-properties@asf + with: + file: ".sdkmanrc" + prefix: "SDKMANRC_" + - name: "Determine Java Version" + run: echo "SDKMANRC_java=${{ env.SDKMANRC_java }}" | sed 's/-.*//' >> $GITHUB_ENV + - name: "☕️ Setup JDK" + uses: actions/setup-java@v5 + with: + distribution: ${{ env.JAVA_DISTRIBUTION }} + java-version: ${{ env.SDKMANRC_java }} + - name: 'Ensure Common Build Date' # to ensure a reproducible build + run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> "$GITHUB_ENV" + - name: "Export gradle.properties properties" + uses: apache/grails-github-actions/export-gradle-properties@asf + - name: "🐘 Setup Gradle" + uses: gradle/actions/setup-gradle@v5 + with: + build-scan-publish: ${{ env.ciBuildScanPublish }} + build-scan-terms-of-use-url: ${{ env.ciBuildScanTermsOfUseUrl }} + build-scan-terms-of-use-agree: ${{ env.ciBuildScanTermsOfUseAgree }} + - name: "📤 Publish Snapshot Artifacts" + env: + GRAILS_PUBLISH_RELEASE: 'false' + MAVEN_PUBLISH_URL: 'https://central.sonatype.com/repository/maven-snapshots/' + MAVEN_PUBLISH_USERNAME: ${{ secrets.NEXUS_PUBLISH_USERNAME }} + MAVEN_PUBLISH_PASSWORD: ${{ secrets.NEXUS_PUBLISH_PASSWORD }} + run: > + ./gradlew publish + --no-build-cache + --rerun-tasks + - name: "📜 Generate Documentation" + run: ./gradlew docs + - name: "🚀 Publish to Github Pages" + uses: apache/grails-github-actions/deploy-github-pages@asf + env: + GRADLE_PUBLISH_RELEASE: 'false' + SOURCE_FOLDER: build/docs diff --git a/.github/workflows/release-notes.yml b/.github/workflows/release-notes.yml new file mode 100644 index 0000000..4951a17 --- /dev/null +++ b/.github/workflows/release-notes.yml @@ -0,0 +1,26 @@ +name: "Release - Drafter" +on: + issues: + types: [ closed,reopened ] + push: + branches: + - '[0-9]+.[0-9]+.x' + - 'main' + pull_request: + types: [ opened, reopened, synchronize, labeled ] + workflow_dispatch: +# queue jobs and only allow 1 run per branch due to the likelihood of hitting GitHub resource limits +concurrency: + group: release-pipeline + cancel-in-progress: false +jobs: + update_release_draft: + permissions: + contents: write # write permission is required to create a github release + pull-requests: write # write permission is required for auto-labeler + runs-on: ubuntu-24.04 + steps: + - name: "📝 Update Release Draft" + uses: release-drafter/release-drafter@v6 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9051e0e..ed65e82 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,90 +1,197 @@ -name: Release +name: "Release" on: - release: - types: [ published ] + release: + types: [ published ] +permissions: { } +env: + # To prevent throttling of the GitHub api, + # include the GitHub token in an environment variable + # since the build will check for it + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GRAILS_PUBLISH_RELEASE: 'true' + JAVA_DISTRIBUTION: 'liberica' + REPO_NAME: ${{ github.event.repository.name }} + RUN_ID: ${{ github.run_id }} + TAG: ${{ github.event.release.tag_name }} + VERSION: 'will be computed in each job' +concurrency: + group: release-pipeline + cancel-in-progress: false jobs: - release: - runs-on: ubuntu-latest + publish: + name: "Stage Jar Files" + permissions: + packages: read # pre-release workflow + contents: write # to create a release + issues: write # to modify milestones + runs-on: ubuntu-24.04 + steps: + - name: "📝 Establish release version" + run: echo "VERSION=${TAG#v}" >> "$GITHUB_ENV" + - name: "Output Agent IP" # in the event RAO blocks this agent, this can be used to debug it + run: curl -s https://api.ipify.org + - name: "📥 Checkout repository" + uses: actions/checkout@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ref: ${{ env.TAG }} + - name: 'Ensure Common Build Date' # to ensure a reproducible build + run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> "$GITHUB_ENV" + - name: "Ensure source files use common date" + run: | + find . -depth \( -type f -o -type d \) -exec touch -d "@${SOURCE_DATE_EPOCH}" {} + + - name: "🔐 Generate key file for artifact signing" env: - GIT_USER_NAME: ${{ secrets.GIT_USER_NAME }} - GIT_USER_EMAIL: ${{ secrets.GIT_USER_EMAIL }} - steps: - - name: Checkout repository - uses: actions/checkout@v2 - with: - token: ${{ secrets.GH_TOKEN }} - - uses: gradle/wrapper-validation-action@v1 - - name: Set up JDK - uses: actions/setup-java@v1 - with: - java-version: 8 - - name: Get latest release version number - id: get_version - uses: battila7/get-version-action@v2 - - name: Run pre-release - uses: micronaut-projects/github-actions/pre-release@master - with: - token: ${{ secrets.GITHUB_TOKEN }} - - name: Publish to Sonatype OSSRH - env: - SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} - SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} - SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} - SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }} - SIGNING_PASSPHRASE: ${{ secrets.SIGNING_PASSPHRASE }} - SECRING_FILE: ${{ secrets.SECRING_FILE }} - RELEASE_VERSION: ${{ steps.get_version.outputs.version-without-v }} - run: | - echo "${SECRING_FILE}" | base64 -d > "${GITHUB_WORKSPACE}/secring.gpg" - echo "Publishing Artifacts for $RELEASE_VERSION" - (set -x; ./gradlew -Pversion="${RELEASE_VERSION}" -Psigning.secretKeyRingFile="${GITHUB_WORKSPACE}/secring.gpg" publishToSonatype closeAndReleaseSonatypeStagingRepository --no-daemon) - - name: Bump patch version by one - uses: actions-ecosystem/action-bump-semver@v1 - id: bump_semver - with: - current_version: ${{steps.get_version.outputs.version-without-v }} - level: patch - - name: Set version in gradle.properties - env: - NEXT_VERSION: ${{ steps.bump_semver.outputs.new_version }} - run: | - echo "Preparing next snapshot" - ./gradlew snapshotVersion -Pversion="${NEXT_VERSION}" - - name: Commit & Push changes - uses: actions-js/push@master - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - author_name: ${{ secrets.GIT_USER_NAME }} - author_email: $${ secrets.GIT_USER_EMAIL }} - message: 'Set version to next SNAPSHOT' - - name: Check file documentation exists - id: check_documentation - uses: andstor/file-existence-action@v1 - with: - files: "src/docs" - - name: Build documentation - if: steps.check_documentation.outputs.files_exists == 'true' - env: - RELEASE_VERSION: ${{ steps.get_version.outputs.version-without-v }} - run: | - ./gradlew asciidoctor -Pversion="${RELEASE_VERSION}" - - name: Export Gradle Properties - uses: micronaut-projects/github-actions/export-gradle-properties@master - - name: Publish to Github Pages - if: steps.check_documentation.outputs.files_exists == 'true' && success() - uses: micronaut-projects/github-pages-deploy-action@master - env: - BETA: ${{ steps.get_version.outputs.isPrerelase }} - TARGET_REPOSITORY: ${{ github.repository }} - GH_TOKEN: ${{ secrets.GH_TOKEN }} - BRANCH: gh-pages - FOLDER: build/asciidoc - DOC_FOLDER: latest - COMMIT_EMAIL: ${{ secrets.GIT_USER_EMAIL }} - COMMIT_NAME: ${{ secrets.GIT_USER_NAME }} - VERSION: ${{ steps.get_version.outputs.version-without-v }} - - name: Run post-release - if: success() - uses: micronaut-projects/github-actions/post-release@master - with: - token: ${{ secrets.GITHUB_TOKEN }} + SECRING_FILE: ${{ secrets.SECRING_FILE }} + run: echo "$SECRING_FILE" | base64 -d > ${{ github.workspace }}/secring.gpg + - name: "Export .sdkmanrc properties" + uses: apache/grails-github-actions/export-gradle-properties@asf + with: + file: ".sdkmanrc" + prefix: "SDKMANRC_" + - name: "Determine Java Version" + run: echo "SDKMANRC_java=${{ env.SDKMANRC_java }}" | sed 's/-.*//' >> $GITHUB_ENV + - name: "☕️ Setup JDK" + uses: actions/setup-java@v5 + with: + distribution: ${{ env.JAVA_DISTRIBUTION }} + java-version: ${{ env.SDKMANRC_java }} + - name: "Export gradle.properties properties" + uses: apache/grails-github-actions/export-gradle-properties@asf + - name: "🐘 Setup Gradle" + uses: gradle/actions/setup-gradle@v5 + with: + build-scan-publish: ${{ env.ciBuildScanPublish }} + build-scan-terms-of-use-url: ${{ env.ciBuildScanTermsOfUseUrl }} + build-scan-terms-of-use-agree: ${{ env.ciBuildScanTermsOfUseAgree }} + - name: "⚙️ Run pre-release" + uses: apache/grails-github-actions/pre-release@asf + env: + RELEASE_VERSION: ${{ env.VERSION }} + - name: "📤 Publish to Maven Central" + env: + NEXUS_PUBLISH_USERNAME: ${{ secrets.NEXUS_PUBLISH_USERNAME }} + NEXUS_PUBLISH_PASSWORD: ${{ secrets.NEXUS_PUBLISH_PASSWORD }} + NEXUS_PUBLISH_URL: 'https://ossrh-staging-api.central.sonatype.com/service/local/' + NEXUS_PUBLISH_DESCRIPTION: '${{ env.REPO_NAME }}:${{ env.VERSION }}:${{ env.RUN_ID }}' + SIGNING_KEY: ${{ secrets.SIGNING_KEY }} + SIGNING_PASSPHRASE: ${{ secrets.SIGNING_PASSPHRASE }} + run: > + ./gradlew + -Psigning.secretKeyRingFile=${{ github.workspace }}/secring.gpg + publishMavenPublicationToSonatypeRepository + closeSonatypeStagingRepository + - name: "Generate Build Date file" + run: echo "$SOURCE_DATE_EPOCH" >> build/BUILD_DATE.txt + - name: "Upload Build Date file" + uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b + with: + files: build/BUILD_DATE.txt + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + release: + name: "Make Release Files Available" + environment: release # this step will be delayed until approved + needs: [ publish ] + runs-on: ubuntu-24.04 + permissions: + contents: read + steps: + - name: "📝 Establish release version" + run: echo "VERSION=${TAG#v}" >> "$GITHUB_ENV" + - name: "📥 Checkout repository" + uses: actions/checkout@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ref: ${{ env.TAG }} + - name: "Export .sdkmanrc properties" + uses: apache/grails-github-actions/export-gradle-properties@asf + with: + file: ".sdkmanrc" + prefix: "SDKMANRC_" + - name: "Determine Java Version" + run: echo "SDKMANRC_java=${{ env.SDKMANRC_java }}" | sed 's/-.*//' >> $GITHUB_ENV + - name: "☕️ Setup JDK" + uses: actions/setup-java@v5 + with: + distribution: ${{ env.JAVA_DISTRIBUTION }} + java-version: ${{ env.SDKMANRC_java }} + - name: "Export gradle.properties properties" + uses: apache/grails-github-actions/export-gradle-properties@asf + - name: "🐘 Setup Gradle" + uses: gradle/actions/setup-gradle@v5 + with: + build-scan-publish: ${{ env.ciBuildScanPublish }} + build-scan-terms-of-use-url: ${{ env.ciBuildScanTermsOfUseUrl }} + build-scan-terms-of-use-agree: ${{ env.ciBuildScanTermsOfUseAgree }} + - name: "📤 Release staging repository" + env: + NEXUS_PUBLISH_USERNAME: ${{ secrets.NEXUS_PUBLISH_USERNAME }} + NEXUS_PUBLISH_PASSWORD: ${{ secrets.NEXUS_PUBLISH_PASSWORD }} + NEXUS_PUBLISH_URL: 'https://ossrh-staging-api.central.sonatype.com/service/local/' + NEXUS_PUBLISH_DESCRIPTION: '${{ env.REPO_NAME }}:${{ env.VERSION }}:${{ env.RUN_ID }}' + run: > + ./gradlew + findSonatypeStagingRepository + releaseSonatypeStagingRepository + docs: + environment: docs # this step will be delayed until approved + name: "Publish Documentation" + needs: [ publish, release ] + runs-on: ubuntu-24.04 + permissions: + contents: write # required to publish documentation to GitHub pages branches + steps: + - name: "📝 Establish release version" + run: echo "VERSION=${TAG#v}" >> "$GITHUB_ENV" + - name: "📥 Checkout repository" + uses: actions/checkout@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ref: ${{ env.TAG }} + - name: "Export .sdkmanrc properties" + uses: apache/grails-github-actions/export-gradle-properties@asf + with: + file: ".sdkmanrc" + prefix: "SDKMANRC_" + - name: "Determine Java Version" + run: echo "SDKMANRC_java=${{ env.SDKMANRC_java }}" | sed 's/-.*//' >> $GITHUB_ENV + - name: "☕️ Setup JDK" + uses: actions/setup-java@v5 + with: + distribution: ${{ env.JAVA_DISTRIBUTION }} + java-version: ${{ env.SDKMANRC_java }} + - name: "Export gradle.properties properties" + uses: apache/grails-github-actions/export-gradle-properties@asf + - name: "🐘 Setup Gradle" + uses: gradle/actions/setup-gradle@v5 + with: + build-scan-publish: ${{ env.ciBuildScanPublish }} + build-scan-terms-of-use-url: ${{ env.ciBuildScanTermsOfUseUrl }} + build-scan-terms-of-use-agree: ${{ env.ciBuildScanTermsOfUseAgree }} + - name: "🔨 Build Documentation" + run: ./gradlew docs + - name: "🚀 Publish to Github Pages" + uses: apache/grails-github-actions/deploy-github-pages@asf + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GRADLE_PUBLISH_RELEASE: 'true' + SOURCE_FOLDER: build/docs + VERSION: ${{ env.VERSION }} + close: + name: "To Next Version" + environment: close # this step will be delayed until approved + needs: [ publish, docs, release ] + runs-on: ubuntu-24.04 + permissions: + contents: write # required for gradle.properties revert + issues: write # required for milestone closing + pull-requests: write # to create the PR that will increment the version + steps: + - name: "📥 Checkout repository" + uses: actions/checkout@v6 + with: + ref: ${{ env.TAG }} + token: ${{ secrets.GITHUB_TOKEN }} + - name: "⚙️ Run post-release" + uses: apache/grails-github-actions/post-release@asf diff --git a/.sdkmanrc b/.sdkmanrc new file mode 100644 index 0000000..03e409c --- /dev/null +++ b/.sdkmanrc @@ -0,0 +1,3 @@ +java=17.0.18-librca +gradle=8.14.4 +groovy=4.0.30 diff --git a/.skills/gradle-best-practices.md b/.skills/gradle-best-practices.md new file mode 100644 index 0000000..58f8693 --- /dev/null +++ b/.skills/gradle-best-practices.md @@ -0,0 +1,254 @@ +# Gradle Best Practices + +## Purpose + +This skill covers Gradle best practices for this project, including convention plugins, extension configuration, +lazy APIs, and build structure. Convention plugins remove duplication across subprojects by centralizing shared +build logic. They live in the `build-logic/` composite build and are applied by ID in each subproject's `build.gradle`. + +## Core Rules + +### NEVER configure subprojects from the root build.gradle + +The root `build.gradle` must NEVER use `subprojects {}`, `allprojects {}`, or `configure(subprojects.matching {...}) {}` +to apply plugins or configure subproject behavior. This is an antipattern that causes ordering issues, breaks project +isolation, and makes builds harder to reason about. + +```groovy +// BAD - Never do this in root build.gradle +subprojects { + apply plugin: 'groovy' + dependencies { + implementation 'org.example:shared-lib:1.0' + } +} + +// BAD - Never do this either +allprojects { + repositories { + mavenCentral() + } +} +``` + +Instead, create a convention plugin in `build-logic/` and apply it in each subproject that needs it: + +```groovy +// GOOD - build-logic/src/main/groovy/config.compile.gradle +plugins { + id 'groovy' +} +// shared compilation config here +``` + +```groovy +// GOOD - plugin/build.gradle +plugins { + id 'config.compile' +} +``` + +The ONLY exception is the `root-publish.gradle` convention plugin, which exists solely as a workaround for a Nexus +publishing bug (https://github.com/gradle-nexus/publish-plugin/issues/310) that requires version/group to be set at the +root level. + +### Use the composite build pattern + +Convention plugins reside in `build-logic/`, which is included as a composite build via `settings.gradle`: + +```groovy +pluginManagement { + includeBuild('./build-logic') { + it.name = 'build-logic' + } +} +``` + +### Naming convention + +Convention plugin files follow the pattern: + +``` +build-logic/src/main/groovy/config..gradle +``` + +The plugin ID matches the filename (minus the `.gradle` extension). For example: + +- `config.compile.gradle` -> plugin ID `config.compile` + +### Declare external plugin dependencies in build-logic/build.gradle + +When a convention plugin applies a third-party plugin, that plugin must be declared as an `implementation` dependency in +`build-logic/build.gradle`: + +```groovy +// build-logic/build.gradle +plugins { + id 'groovy-gradle-plugin' +} + +dependencies { + implementation platform("org.apache.grails:grails-bom:${gradleProperties.grailsVersion}") + implementation 'org.apache.grails:grails-gradle-plugins' + implementation "com.adarshr:gradle-test-logger-plugin:${gradleProperties.testLoggerVersion}" + implementation 'cloud.wondrify:asset-pipeline-gradle' + implementation 'org.apache.grails.gradle:grails-publish' +} +``` + +### Share properties from root gradle.properties + +The `build-logic/build.gradle` reads the root `gradle.properties` and exposes those values as extra properties so +convention plugins can reference them (e.g., `grailsVersion`): + +```groovy +file('../gradle.properties').withInputStream { is -> + extensions.extraProperties.set( + 'gradleProperties', + new Properties().tap { load(is) } + ) +} + +allprojects { project -> + gradleProperties.stringPropertyNames().each { key -> + project.extensions.extraProperties.set( + key, + gradleProperties.getProperty(key) + ) + } +} +``` + +## Avoid Eager Initialization + +Always use lazy/deferred APIs to avoid eagerly resolving tasks or configurations: + +```groovy +// GOOD - lazy task configuration +tasks.withType(JavaCompile).configureEach { + options.encoding = StandardCharsets.UTF_8.name() +} + +tasks.named('bootRun', JavaExec) { + doFirst { /* ... */ } +} + +tasks.register('docs') { + dependsOn(/* ... */) +} + +// BAD - eager resolution +tasks.withType(JavaCompile) { // missing .configureEach + options.encoding = 'UTF-8' +} + +task docs { // old task() API is eager + dependsOn /* ... */ +} +``` + +Key APIs to use: + +- `tasks.register()` instead of `task()` +- `tasks.named()` instead of `tasks.getByName()` +- `tasks.withType(X).configureEach {}` instead of `tasks.withType(X) {}` +- `project.provider {}` for lazy values +- `layout.buildDirectory` instead of `buildDir` +- `dependsOn()` method instead of `dependsOn =` setter (setter replaces all dependencies; the method adds to them) +- Do NOT chain `.configure {}` on `tasks.register()` or `tasks.named()` — pass the closure directly to preserve type hints + +## Extension Configuration with Type Hints + +When configuring project extensions (like publishing metadata or third-party plugin configurations), use +`extensions.configure(Type)` with explicit `it` for type hints and better IDE support: + +```groovy +// GOOD - explicit it in extensions.configure() for type hints +extensions.configure(GrailsPublishExtension) { + it.artifactId = project.name + it.githubSlug = 'gpc/taggable' + it.license.name = 'Apache-2.0' + it.title = 'My Plugin' + it.developers = [name: 'Developer Name'] +} +``` + +Explicit `it` is NOT required in `tasks.named()`, `tasks.register()`, or `configureEach` — these already have typed +delegates: + +```groovy +// GOOD - no explicit it needed, delegate is already typed +tasks.withType(Checkstyle).configureEach { + group = 'verification' + onlyIf { !project.hasProperty('skipCodeStyle') } +} + +tasks.named('bootRun', JavaExec) { + doFirst { + jvmArgs("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005") + } +} +``` + +**Benefits of `extensions.configure(Type)` with explicit `it`:** + +- IDE auto-completion and type-checking for extension properties +- Clearer intent: code readers immediately see the extension type being configured +- Reduces runtime errors from typos in property names + +## Composition Over Inheritance + +Convention plugins should compose by applying other convention plugins rather than duplicating logic: + +```groovy +// example.gradle applies other convention plugins +plugins { + id 'org.apache.grails.gradle.grails-web' + id 'org.apache.grails.gradle.grails-gsp' + id 'config.grails-assets' + id 'config.app-run' +} +``` + +## Existing Convention Plugins + +| Plugin | Purpose | +|----------------------------------|--------------------------------------------------------------------------------------| +| `app-run.gradle` | Debug flags for `bootRun` | +| `code-coverage.gradle` | JaCoCo coverage for project (XML + HTML reports) | +| `code-coverage-aggregate.gradle` | JaCoCo coverage aggregation across subprojects (XML + HTML reports) | +| `code-style.gradle` | Checkstyle + CodeNarc code style checking (configs in `build-logic/config/`) | +| `compile.gradle` | Java/Groovy compilation settings (UTF-8, incremental, Java release from `.sdkmanrc`) | +| `docs.gradle` | Documentation aggregation (Groovydoc + Asciidoctor) | +| `example-app.gradle` | Example app config (grails-web, GSP, assets) | +| `grails-assets.gradle` | Asset pipeline with Bootstrap/jQuery WebJars | +| `grails-plugin.gradle` | Grails plugin application | +| `publish.gradle` | Per-project Maven publishing metadata | +| `publish-root.gradle` | Root-level Nexus publishing workaround | +| `testing.gradle` | Test framework config (Spock, JUnit Platform, test-logger) | + +## When to Create a New Convention Plugin + +Create a new convention plugin when: + +- Two or more subprojects share the same build configuration +- A subproject's `build.gradle` grows beyond applying plugins and declaring dependencies +- You need to enforce a project-wide standard (e.g., code formatting, static analysis) + +Keep each convention plugin focused on a single concern. Prefer small, composable plugins over monolithic ones. + +## Repository Management + +Repositories are managed centrally in `settings.gradle` via `dependencyResolutionManagement`: + +```groovy +dependencyResolutionManagement { + repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS + repositories { + maven { url = 'https://repo.grails.org/grails/restricted' } + } +} +``` + +This prevents subprojects from declaring their own repositories, ensuring consistency. The `FAIL_ON_PROJECT_REPOS` mode +enforces this. diff --git a/.skills/plugin-project.md b/.skills/plugin-project.md new file mode 100644 index 0000000..956ef87 --- /dev/null +++ b/.skills/plugin-project.md @@ -0,0 +1,145 @@ +# Plugin Project Best Practices + +## Purpose + +The `plugin/` directory contains the Grails plugin artifact. It is the only publishable library in this repository. It +must contain ONLY the plugin source code and unit tests; nothing else. + +## Core Rules + +### Plugin project contains ONLY plugin code and unit tests + +The plugin project (`plugin/`) must contain: + +- Plugin source code under `src/main/groovy/` and `grails-app/` +- Unit tests under `src/test/groovy/` +- Plugin configuration files under `grails-app/conf/` + +The plugin project must NOT contain: + +- Integration tests (these belong in example apps under `examples/`) +- Functional tests (these belong in example apps under `examples/`) +- Example controllers, views, or domain classes +- Test controllers or test-specific artifacts +- Application-level configuration not related to the plugin (e.g., database config, asset config) + +### Why: separation of concerns + +Keeping integration/functional tests out of the plugin project ensures: + +1. The plugin artifact is clean – no test dependencies or test code leaks into the published JAR +2. Tests that require a running Grails application exercise the plugin as a real consumer would +3. The plugin's API surface is validated from the outside, not the inside +4. Different example apps can test different configurations of the plugin + +## Project Structure + +``` +plugin/ +├── build.gradle # Only convention plugins + dependencies +├── grails-app/ +│ ├── conf/ +│ │ ├── application.yml # Plugin-specific config defaults +│ ├── controllers/ # Interceptors, controller-scoped artifacts +│ │ └── org/grails/plugins/servertiming/ +│ │ └── ServerTimingInterceptor.groovy +└── src/ + ├── main/groovy/ # Core plugin classes + │ └── org/grails/plugins/servertiming/ + │ ├── ServerTimingAutoConfiguration.groovy + │ ├── ServerTimingFilter.groovy + │ ├── ServerTimingGrailsPlugin.groovy + │ ├── ServerTimingResponseWrapper.groovy + │ ├── config/ + │ │ ├── EnabledCondition.groovy + │ │ └── ServerTimingConfig.groovy + │ └── core/ + │ ├── Metric.groovy + │ └── TimingMetric.groovy + ├── main/resources/ + │ ├── META-INF/spring + │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports + │ └── spring-configuration-metadata.json + └── test/groovy/ # Unit tests ONLY + └── org/grails/plugins/servertiming/ + ├── MetricSpec.groovy + └── TimingMetricSpec.groovy +``` + +## build.gradle Pattern + +The plugin's `build.gradle` should be minimal -- apply convention plugins and declare dependencies: + +```groovy +plugins { + id 'config.compile' + id 'config.testing' + id 'config.grails-plugin' + id 'config.publish' +} + +version = projectVersion +group = 'io.github.gpc' + +dependencies { + + profile 'org.apache.grails.profiles:web-plugin' + console 'org.apache.grails:grails-console' + + compileOnly platform("org.apache.grails:grails-bom:$grailsVersion") + compileOnly 'org.apache.grails:grails-dependencies-starter-web' + + testImplementation platform("org.apache.grails:grails-bom:$grailsVersion") + testImplementation 'org.apache.grails:grails-dependencies-starter-web' + testImplementation 'org.apache.grails:grails-dependencies-test' +} +``` + +Key patterns: + +- Use `compileOnly` for framework dependencies the consuming application will provide +- Use `testImplementation` for test-only dependencies +- Apply `config.publish` to configure Maven publishing metadata +- NEVER add custom task configuration here – move it to a convention plugin + +## Unit Test Guidelines + +Unit tests in the plugin project test individual classes in isolation: + +- Test domain logic, validation, and data structures (e.g., `Tag`, `TagLink`) +- Test utility classes and traits (e.g., `Taggable`) +- Use Spock Framework with `@Unroll` for data-driven tests +- Do NOT start the Grails application context for unit tests +- Do NOT make HTTP requests in unit tests +- Do NOT test controller actions, interceptors, or filters end-to-end in the plugin project + +### What belongs in unit tests + +- `Tag` name constraints (uniqueness, blank, length) +- `TagLink` polymorphic mapping validation +- `Taggable` trait instance methods (`addTag`, `removeTag`, `parseTags`, etc.) when isolated +- `TaggableService` helpers that do not require a running context +- Equals/hashCode contracts on domain entities +- Validation error cases + +### What does NOT belong in unit tests + +- Testing that `Taggable` static methods query correctly against a real datastore (integration test) +- Testing that `TaggableService.refreshDomainClasses()` wires up domain class families in a running app (integration test) +- Testing `` rendering in a GSP view (functional test) +- Testing behavior with a real Hibernate session, GSP views, JSON rendering, or static assets (functional test) + +## Plugin Descriptor + +The `TaggableGrailsPlugin` class extends `grails.plugins.Plugin` and exposes important +information about the plugin to the Grails framework. + +## Dependency Scoping + +- **`compileOnly`**: Framework dependencies the host app provides (Grails web, servlet API) +- **`implementation`**: Dependencies the plugin bundles and needs at runtime (use sparingly) +- **`testImplementation`**: Test framework dependencies (Spock, grails-testing-support) +- **`console`**: Grails console support +- **`profile`**: The Grails profile (web-plugin for plugins) + +Avoid `implementation` for Grails/Spring/Servlet dependencies -- the consuming application provides these. diff --git a/.skills/repository-structure.md b/.skills/repository-structure.md new file mode 100644 index 0000000..c07cbb8 --- /dev/null +++ b/.skills/repository-structure.md @@ -0,0 +1,216 @@ +# Grails Plugin Repository Structure + +## Purpose + +This document defines the canonical structure for Grails plugin repositories. The structure enforces separation of +concerns: the plugin project contains only library code and unit tests, example apps provide integration/functional test +coverage, and build logic is centralized in convention plugins. + +## Directory Layout + +``` +taggable/ +├── .github/ # CI/CD workflows and GitHub config +│ ├── workflows/ +│ │ ├── ci.yml # Build, test, publish snapshots +│ │ ├── code-coverage.yml # Create a code coverage report +│ │ ├── code-style.yml # Check code style +│ │ ├── release.yml # Multi-stage release pipeline +│ │ └── release-notes.yml # Automated release draft notes +│ ├── release-drafter.yml # Release drafter categories/labels +│ └── dependency-graph/ +│ └── external-references.yml # Maven Central package association +│ +├── build-logic/ # Gradle convention plugins (composite build) +│ ├── build.gradle # Plugin dependencies (groovy-gradle-plugin) +│ ├── settings.gradle # Build-logic project settings +│ ├── config/ # Shared code style config files +│ │ ├── checkstyle/ # Checkstyle XML configs +│ │ └── codenarc/ # CodeNarc ruleset +│ └── src/main/groovy/ # Convention plugin files (*.gradle) +│ ├── config.app-run.gradle +│ ├── config.code-coverage.gradle +│ ├── config.code-coverage-aggregate.gradle +│ ├── config.code-style.gradle +│ ├── config.compile.gradle +│ ├── config.docs.gradle +│ ├── config.example-app.gradle +│ ├── config.grails-assets.gradle +│ ├── config.grails-plugin.gradle +│ ├── config.publish.gradle +│ ├── config.publish-root.gradle +│ └── config.testing.gradle +│ +├── plugin/ # The Grails plugin artifact +│ ├── build.gradle # Convention plugins + dependencies only +│ ├── grails-app/ +│ │ ├── conf/ # Plugin config (application.yml, logback) +│ │ └── controllers/ # Interceptors and controller artifacts +│ └── src/ +│ ├── main/groovy/ # Plugin source code +│ └── test/groovy/ # Unit tests ONLY +│ +├── examples/ # Example apps (auto-discovered) +│ ├── app1/ # first example app with the plugin enabled +│ │ ├── build.gradle +│ │ ├── grails-app/ # Standard Grails app structure +│ │ │ ├── conf/ +│ │ │ ├── controllers/ # Test controllers +│ │ │ ├── views/ # Test views (GSP) +│ │ │ ├── init/ +│ │ │ ├── assets/ +│ │ │ └── i18n/ +│ │ └── src/ +│ │ └── integration-test/ # Integration & functional tests +│ └── app2/ # second app showing disable feature +│ ├── build.gradle +│ ├── grails-app/ # Standard Grails app structure +│ │ ├── conf/ +│ │ ├── controllers/ # Test controllers +│ │ ├── views/ # Test views (GSP) +│ │ ├── init/ +│ │ ├── assets/ +│ │ └── i18n/ +│ └── src/ +│ └── integration-test/ # Integration & functional tests +│ +├── code-coverage/ # JaCoCo coverage aggregation +│ └── build.gradle # Declares which projects contribute coverage data +│ +├── docs/ # Asciidoctor documentation +│ ├── build.gradle +│ └── src/docs/ # .adoc source files +│ +├── build.gradle # Root build (docs + root-publish ONLY) +├── settings.gradle # Multi-project settings + composite build +├── gradle.properties # Shared version properties +├── .sdkmanrc # SDK versions (Java, Gradle, Groovy) +├── AGENTS.md # AI agent instructions +├── .skills/ # Best practice skill files +├── LICENSE # Apache 2.0 +└── README.md +``` + +## Key Architectural Rules + +### 1. Root build.gradle is minimal + +The root `build.gradle` applies only root-level convention plugins (docs aggregation, root-publish workaround). It must +NEVER use `subprojects {}`, `allprojects {}`, or any mechanism to configure child projects. All shared configuration +flows through convention plugins. + +```groovy +// Root build.gradle -- this is all that should be here +plugins { + id 'idea' + id 'config.docs' + id 'config.root-publish' +} +``` + +### 2. Plugin project = library code + unit tests + +The `plugin/` project is the published artifact. It contains: + +- Source code (`src/main/groovy/`, `grails-app/`) +- Unit tests (`src/test/groovy/`) + +It does NOT contain integration tests, functional tests, example controllers, or test views. + +### 3. Example apps = integration/functional tests + +All tests requiring a running Grails application live in example apps under `examples/`. Each app: + +- Depends on the plugin via `implementation project(':taggable')` +- Contains test controllers and views that exercise the plugin +- Contains integration tests under `src/integration-test/` +- Is auto-discovered by `settings.gradle` + +### 4. Build logic is centralized + +Convention plugins in `build-logic/` eliminate all duplication: + +- Compilation settings: `config.compile.gradle` +- Test configuration: `config.testing.gradle` +- Plugin setup: `config.grails-plugin.gradle` +- Example app setup: `config.example-app.gradle` +- Publishing: `config.publish.gradle` +- Coverage aggregation: `config.coverage-aggregate.gradle` +- Code style checking: `config.code-style.gradle` + +### 5. Centralized dependency resolution + +Repositories are declared once in `settings.gradle` using `dependencyResolutionManagement`. The `FAIL_ON_PROJECT_REPOS` +mode prevents subprojects from declaring their own repositories. + +### 6. Shared properties via gradle.properties + +Version numbers and shared settings live in `gradle.properties` at the root: + +```properties +projectVersion=0.0.1-SNAPSHOT +grailsVersion=7.0.7 +``` + +These are available in all subprojects as project properties (`projectVersion`, `grailsVersion`). + +## Adding a New Example App + +1. Create a new directory under `examples/` (e.g., `examples/app2/`) +2. Add a `build.gradle` applying the convention plugins: + ```groovy + plugins { + id 'config.example-app' + } + ``` +3. Add standard Grails app structure under `grails-app/` +4. Add integration tests under `src/integration-test/groovy/` +5. The app will be auto-discovered by `settings.gradle` and automatically included in coverage aggregation -- no manual + registration needed + +## Adding a New Convention Plugin + +1. Create a new file: `build-logic/src/main/groovy/config..gradle` +2. If the plugin applies third-party plugins, add their dependencies to `build-logic/build.gradle` +3. Apply the new plugin ID in the relevant subproject(s) +4. Keep the plugin focused on a single concern + +## Build Commands + +```bash +# Full build (all subprojects) +./gradlew build + +# Plugin unit tests only +./gradlew :taggable:test + +# Example app integration tests +./gradlew :app1:integrationTest + +# Aggregated coverage report (unit + integration) +./gradlew jacocoAggregatedReport + +# Run an example app +./gradlew :app1:bootRun + +# Generate documentation +./gradlew docs + +# Clean everything +./gradlew clean + +# Skip tests +./gradlew build -PskipTests +``` + +## SDK Management + +The `.sdkmanrc` file pins exact SDK versions. Run `sdk env install` to install them. CI reads `.sdkmanrc` to determine +the Java version dynamically. + +## CI/CD Pipeline + +- **CI**: Builds and tests on every push/PR. Publishes snapshots on push to release branches. +- **Coverage**: Runs the full build and posts an aggregated JaCoCo coverage summary to the GitHub Actions job summary. +- **Release**: 4-stage pipeline (stage -> release -> docs -> version bump) triggered by GitHub release. +- **Release Notes**: Auto-drafts release notes from PRs using release-drafter with category labels. diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 48b31d6..0000000 --- a/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -language: groovy -jdk: -- oraclejdk7 -before_script: -- rm -rf target -script: ./travis-build.sh -env: - global: - - GIT_NAME="Graeme Rocher" - - GIT_EMAIL="graeme.rocher@gmail.com" - - secure: nDFWKPneybRf6eS5g/UimKxizX7Z/EyJgq6Zz++OIfENuLTK5ZfXNgeCB0E3CVQ/EsO1pZIi6rJz27s9sDeneBgmBdoKu4hAIVHk5jwL26SKFd1G3zy9+JHEGrYlgC4pcxKydWuhm7euqJT2ighuSlSIPjcSR9ep+87moHLv3NY= - - secure: MdCohHHlR+BT0YSkyqddr7ejNCwWNeg0epuPkCqoFRczOdNnT+TGeWrhxMzQVYPNpe2QG76sqrWwyGtJRAn6/Gch7bYfpHkoGWWsacm2VGXLPXucjyz2vVg+ZzWGirkssFfFVJsG33mljzhxjljGviKDgsVS2DYwRBO9OKQYqXY= - - secure: QOozG0LmSnIeslpKao6iudVCZ/MLGoI1nkELLDMer1EkZMOOD2jjWuTI1OvGeD2RKzeS2e78iyvXm5ohSSoAVqZ+IAKdG0SQWw/vXNUJV1PkgPO57DNnp5iyGwVh9vK+Q+iUy9veoRiaX91aWL5JzXobjHOxf7YcCjnYMZ/fTuU= - - secure: DOUGG6+sqQWYJlc8oa2Faio2lgMhU+gOo0eAGpn20omf0v/+4Wa/YqmaqW77TM0bRxtx0Qt1S8f2R4RfSum2M58Kn5FuNJPOFSycpRdUv7A5tmJ+aNtCUpx2oBOp4q31exQH4K3KPRidVE7DQDMPX7KkvvLD4G9C6V6qZUQ4SAY= - - secure: Pr2loG4v2x4xH/+K8NImt+Geo3wT7chY4tiCOXWnuqID/jU2CIRoEpk8df3YS3S2MtStnDEMMDWgMx+3rfCURMEGlr9Nd7z5XkukcgnWpbB4YEm2vx/B37cGhGmbeGJdQTanPvcG8HyoJuBlkhUEAFJ6kNtfcUZWnmtIj7/EfCM= -sudo: false -cache: - directories: - - $HOME/.gradle diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..29a9231 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,202 @@ +# AGENTS.md - taggable + +## Project Overview + +This is a **Grails Plugin** that adds a generic tagging mechanism to any Grails domain class. Domain classes implement +the `Taggable` trait to gain instance and static methods for attaching, querying, and counting tags, plus a taglib for +rendering a frequency-ranked tag cloud. + +- **Language:** Groovy 4.0.30 on Java 17 +- **Framework:** Grails 7.x +- **Build System:** Gradle 8.14.4 (with wrapper) +- **Current Version:** 7.0.x-SNAPSHOT +- **License:** Apache 2.0 + +## Skill Files (Best Practices) + +Detailed best practices are documented in `.skills/`: + +| Skill File | Purpose | +|------------------------------------------------------------------------|-------------------------------------------------------| +| [`.skills/repository-structure.md`](.skills/repository-structure.md) | Canonical directory layout and architectural rules | +| [`.skills/gradle-best-practices.md`](.skills/gradle-best-practices.md) | Gradle best practices, convention plugins, and idioms | +| [`.skills/plugin-project.md`](.skills/plugin-project.md) | Plugin project scope: source code + unit tests only | +| [`.skills/example-apps.md`](.skills/example-apps.md) | Example app patterns: integration & functional tests | + +**Read these skill files before making structural changes to the repository.** + +## Critical Rules + +1. **NEVER add code to the root `build.gradle` to configure subprojects.** No `subprojects {}`, `allprojects {}`, or + `configure()` blocks. All shared configuration goes through convention plugins in `build-logic/`. +2. **The plugin project contains ONLY plugin code and unit tests.** No integration tests, no functional tests, no + example controllers or views. +3. **Example apps under `examples/` host all integration and functional tests.** They depend on the plugin via + `implementation project(':taggable')` and test it as a real consumer would. +4. **Use Gradle convention plugins to deduplicate.** If two or more subprojects share build logic, extract it into a + convention plugin in `build-logic/`. +5. **Always use lazy Gradle APIs** to avoid eager initialization (`tasks.register()`, `tasks.named()`, `configureEach`, + `provider {}`). + +## Repository Structure + +``` +taggable/ +├── .skills/ # Best practice skill files +├── plugin/ # Core Grails plugin (artifact: taggable) +│ ├── grails-app/ # Plugin domain classes, services, and taglibs +│ └── src/main/ # Plugin source code (Taggable trait, plugin descriptor) +├── examples/app1/ # Example Grails app +│ └── grails-app/ # Domain classes and conf for integration testing +├── docs/ # Asciidoctor documentation +├── build-logic/ # Gradle convention plugins (composite build) +├── .github/workflows/ # CI, release, and release-notes workflows +├── build.gradle # Root build file (docs + root-publish ONLY) +├── settings.gradle # Multi-project settings +└── gradle.properties # Version properties +``` + +## Build and Test Commands + +```bash +# Full build (compile + test) +./gradlew build + +# Run only unit tests (plugin module) +./gradlew :taggable:test + +# Run integration tests (example app) +./gradlew :app1:integrationTest + +# Skip tests +./gradlew build -PskipTests + +# Run the example app +./gradlew :app1:bootRun + +# Generate documentation +./gradlew docs + +# Clean build +./gradlew clean build + +# Run code style checks only +./gradlew codeStyle + +# Skip code style checks +./gradlew build -PskipCodeStyle +``` + +## SDK Requirements + +Use SDKMAN to install the correct tool versions (see `.sdkmanrc`): + +- Java: `17.0.18-librca` +- Gradle: `8.14.4` +- Groovy: `4.0.30` + +Run `sdk env install` to set up the environment. + +## Architecture + +The plugin provides a trait-based tagging mechanism with a supporting service, domain model, and tag library: + +1. **`TaggableGrailsPlugin`** is the plugin descriptor. It extends `grails.plugins.Plugin`, observes `hibernate`, and + calls `taggableService.refreshDomainClasses()` on startup and `onChange` so the domain class family map stays in + sync with reloaded artifacts. +2. **`Taggable`** is a Groovy trait that domain classes implement to become taggable. It provides instance methods + like `addTag(name)`, `addTags(names)`, `removeTag(name)`, `setTags(list)`, `parseTags(str, delimiter)`, and + `getTags()`, plus static methods `findAllByTag(name)`, `findAllByTagWithCriteria(name, closure)`, + `countByTag(name)`, `getAllTags()`, `getTotalTags()`, and `findAllTagsWithCriteria(params, closure)`. +3. **`TaggableService`** maintains `domainClassFamilies` — a map from class name to the property names of that class + plus all of its subclasses — so polymorphic queries against `TagLink` can include subclass references. It exposes + `getTagCounts(type)` for aggregation and `refreshDomainClasses()` which is called by the plugin lifecycle. +4. **`Tag`** and **`TagLink`** are the domain entities. `Tag` stores the unique tag name; `TagLink` is the polymorphic + join table with `tag`, `tagRef` (the id of the tagged instance), and `type` (the simple class name of the tagged + domain class). +5. **`TagsTagLib`** (namespace `tags`) provides `` for rendering a frequency-ranked tag cloud where CSS + class names scale with tag frequency. + +### Core Classes + +| Class / Interface | Location | Purpose | +|-------------------------|-------------------------------------------------------|--------------------------------------------------------------------| +| `TaggableGrailsPlugin` | `plugin/src/main/groovy/grails/plugins/taggable/` | Plugin descriptor; refreshes domain class families on load/change | +| `Taggable` | `plugin/src/main/groovy/grails/plugins/taggable/` | Trait that adds tagging methods to a domain class | +| `TagException` | `plugin/src/main/groovy/grails/plugins/taggable/` | Thrown when a tag is in an invalid state | +| `Tag` | `plugin/grails-app/domain/grails/plugins/taggable/` | Domain entity storing the unique tag name | +| `TagLink` | `plugin/grails-app/domain/grails/plugins/taggable/` | Polymorphic join entity (`tag`, `tagRef`, `type`) | +| `TaggableService` | `plugin/grails-app/services/grails/plugins/taggable/` | Maintains `domainClassFamilies`; exposes `getTagCounts(type)` | +| `TagsTagLib` | `plugin/grails-app/taglib/grails/plugins/taggable/` | Tag library: `` | + +## Configuration + +The plugin reads the following keys from the application config (for example, `application.yml` or `application.groovy`): + +| Key | Type | Default | Purpose | +|------------------------------------|--------------|--------------------------------------------------------|-----------------------------------------------------------------| +| `grails.taggable.preserve.case` | boolean | `false` | When `false`, tag names are normalized to lowercase | +| `grails.taggable.tag.table` | string | (Hibernate default) | Overrides the physical table name for `Tag` | +| `grails.taggable.tagLink.table` | string | (Hibernate default) | Overrides the physical table name for `TagLink` | +| `grails.taggable.tag.autoImport` | boolean | (Hibernate default) | Toggle Hibernate `autoImport` for `Tag` | +| `grails.taggable.tagLink.autoImport` | boolean | (Hibernate default) | Toggle Hibernate `autoImport` for `TagLink` | +| `grails.taggable.css.classes` | list | `['smallest','small','medium','large','largest']` | CSS classes used by `` to scale by frequency | + +Example `application.groovy`: + +```groovy +grails { + taggable { + preserve.case = false + css.classes = ['tag-xs', 'tag-sm', 'tag-md', 'tag-lg', 'tag-xl'] + tag.table = 'my_tag' + tagLink.table = 'my_tag_link' + } +} +``` + +## Testing + +### Unit Tests (`plugin/src/test/`) + +Unit tests use the **Spock Framework** and run on JUnit Platform. + +### Integration / Functional Tests (`examples/app1/`) + +The example app under `examples/app1/` declares taggable domain classes and exercises the `Taggable` trait, the +`TaggableService`, and the `` taglib against a real datastore. Integration and functional tests added +here depend on the plugin as a real consumer would. + +## Build-Logic Convention Plugins + +Convention plugins in `build-logic/src/main/groovy/` standardize build configuration: + +| Plugin | Purpose | +|--------------------------|--------------------------------------------------------------------------------------| +| `app-run.gradle` | Debug flags for `bootRun` | +| `compile.gradle` | Java/Groovy compilation settings (UTF-8, incremental, Java release from `.sdkmanrc`) | +| `docs.gradle` | Documentation aggregation (Groovydoc + Asciidoctor) | +| `example-app.gradle` | Example app config (grails-web, GSP, assets) | +| `grails-assets.gradle` | Asset pipeline with Bootstrap/jQuery WebJars | +| `grails-plugin.gradle` | Grails plugin application | +| `publish.gradle` | Per-project Maven publishing metadata | +| `publish-root.gradle` | Root-level Nexus publishing workaround | +| `testing.gradle` | Test framework config (Spock, JUnit Platform, test-logger) | + +## CI/CD + +- **CI** (`.github/workflows/ci.yml`): Builds and tests on push/PR; publishes snapshots to Maven Central Snapshots on + push to release branches. +- **Release** (`.github/workflows/release.yml`): 4-stage pipeline triggered by GitHub release — stage artifacts, release + to Maven Central, publish docs to GitHub Pages, bump version. +- **Release Notes** (`.github/workflows/release-notes.yml`): Auto-drafts release notes using release-drafter with + category labels. + +## Code Conventions + +- Groovy source files use standard Grails conventions (services, domain classes, and taglibs in `grails-app/`, other + classes in `src/main/groovy/`). +- **Use `def` for local variables** where the type is inferred from the right-hand side (e.g., constructor calls, + method calls, casts, factory methods). Explicit types should only be used for local variables when the type cannot + be inferred or when needed for `@CompileStatic` compilation. This applies to both production code and tests. +- When writing Gradle, always use the latest best practices to avoid eager initialization. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..129720c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,163 @@ +# Contributing to Taggable + +Thank you for your interest in contributing! This guide will help you get started. + +## Code of Conduct + +This project has a [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold it. Please report +unacceptable behavior to the project maintainers. + +## Getting Started + +### Prerequisites + +Install [SDKMAN!](https://sdkman.io/) to manage JDK, Gradle, and Groovy versions: + +```bash +curl -s "https://get.sdkman.io" | bash +``` + +### Setting Up the Development Environment + +```bash +# Clone the repository +git clone https://github.com/gpc/taggable.git +cd taggable + +# Install the required SDK versions (Java 17, Gradle 8.14.4, Groovy 4.0.30) +sdk env install + +# Build the project +./gradlew build +``` + +### Project Structure + +``` +taggable/ +├── plugin/ # The publishable Grails plugin (source + unit tests ONLY) +├── examples/app1/ # Example app with integration tests +├── build-logic/ # Gradle convention plugins (shared build configuration) +├── docs/ # Asciidoctor documentation +└── .skills/ # AI agent best-practice docs +``` + +Key architectural rules: + +- **Plugin module** contains only plugin source code and unit tests – no integration tests, no example controllers. +- **Example apps** under `examples/` host all integration and functional tests. They depend on the plugin as a real + consumer would. +- **Convention plugins** in `build-logic/` deduplicate build configuration. Never use `subprojects {}`, + `allprojects {}`, or `configure()` blocks in the root `build.gradle`. + +## Building and Testing + +```bash +# Full build (compile + all tests) +./gradlew build + +# Plugin unit tests only +./gradlew :taggable:test + +# Integration tests (runs the example app) +./gradlew :app1:integrationTest + +# Run the example app locally +./gradlew :app1:bootRun + +# Generate documentation +./gradlew docs + +# Skip tests +./gradlew build -PskipTests + +# Clean build +./gradlew clean build +``` + +### Code Coverage + +The project uses JaCoCo to aggregate coverage data from both plugin unit tests and example app integration tests. + +```bash +# Generate the aggregated coverage report +./gradlew jacocoAggregatedReport +``` + +Reports are generated at: + +| Report | Location | +|---------------------------------|----------------------------------------------------------------------------------| +| Aggregated (unit + integration) | `code-coverage/build/reports/jacoco/jacocoAggregatedReport/html/index.html` | +| Plugin unit tests | `plugin/build/reports/jacoco/test/html/index.html` | +| App1 integration tests | `examples/app1/build/reports/jacoco/jacocoIntegrationTestReport/html/index.html` | + +The aggregated report is also produced automatically as part of `./gradlew build`, so you can view it after any full +build. + +## Making Changes + +### Branching Strategy + +- Create a feature branch from the current release branch (e.g., `7.0.x`): + - `feature/short-description` for new features + - `fix/short-description` for bug fixes + - `docs/short-description` for documentation changes + - `refactor/short-description` for refactoring + +These branch prefixes are used by [release-drafter](https://github.com/release-drafter/release-drafter) to automatically +categorize changes in release notes. + +### Coding Standards + +- **Language:** Groovy 4.0 on Java 17 +- **Framework:** Grails 7.0 +- **Testing:** Spock Framework on JUnit Platform +- Follow existing code conventions in the project +- Keep the `Taggable` trait API backwards compatible — applications rely on the instance and static methods it adds to + their domain classes +- Preserve polymorphic `TagLink` semantics: any change to how `type` / `tagRef` is resolved must be reflected in + `TaggableService.domainClassFamilies` + +### Gradle Conventions + +- Always use lazy APIs: `tasks.register()`, `tasks.named()`, `configureEach`, `provider {}` +- Never use eager task creation (`tasks.create()`, `project.task()`) +- If two or more subprojects share build logic, extract it into a convention plugin in `build-logic/` + +## Submitting a Pull Request + +1. **Ensure all tests pass** locally: `./gradlew build` +2. **Write tests** for new functionality: + - Unit tests go in `plugin/src/test/` + - Integration tests go in `examples/app1/src/integration-test/` +3. **Update documentation** if you changed behavior or added features (in `docs/src/docs/`) +4. **Push your branch** and open a pull request against the release branch +5. **Fill out the PR template** completely + +### What to Expect + +- CI will run automatically on your PR +- A maintainer will review your changes +- You may be asked to make revisions +- Once approved, a maintainer will merge your PR + +## Reporting Issues + +- Use + the [bug report template](https://github.com/gpc/taggable/issues/new?template=bug_report.yml) + for bugs +- Use + the [feature request template](https://github.com/gpc/taggable/issues/new?template=feature_request.yml) + for enhancements +- Check [existing issues](https://github.com/gpc/taggable/issues) before creating a new one + +## Security Vulnerabilities + +If you discover a security vulnerability, **do not open a public issue**. Please see [SECURITY.md](.github/SECURITY.md) +for responsible disclosure instructions. + +## License + +By contributing to this project, you agree that your contributions will be licensed under +the [Apache License 2.0](LICENSE). diff --git a/README.md b/README.md index 67f0fdf..4f40c96 100644 --- a/README.md +++ b/README.md @@ -15,18 +15,17 @@ Classes can be made taggable by implementing the [grails.plugins.taggable.Taggab Requirements ------------ -Grails Version: 4.0.0 +Grails 7.0.x, Groovy 4.0.x, Java 17+ Installation ------------ -Add this dependency to `build.gradle` +Add this dependency to `build.gradle`: ```groovy - dependencies { - compile "io.github.gpc:taggable:4.0.0" +dependencies { + implementation 'io.github.gpc:taggable:7.0.0' } - ``` By default, the plugin will force all tags to lower case. If you want to preserve the case of tags, you must specify the diff --git a/build-logic/build.gradle b/build-logic/build.gradle new file mode 100644 index 0000000..746607d --- /dev/null +++ b/build-logic/build.gradle @@ -0,0 +1,29 @@ +plugins { + id 'groovy-gradle-plugin' +} + +file('../gradle.properties').withInputStream { is -> + extensions.extraProperties.set( + 'gradleProperties', + new Properties().tap { load(is) } + ) +} + +allprojects { project -> + gradleProperties.stringPropertyNames().each { key -> + project.extensions.extraProperties.set( + key, + gradleProperties.getProperty(key) + ) + } +} + +dependencies { + implementation platform("org.apache.grails:grails-bom:${gradleProperties.grailsVersion}") + implementation 'cloud.wondrify:asset-pipeline-gradle' + implementation "com.adarshr:gradle-test-logger-plugin:${gradleProperties.testLoggerVersion}" + implementation 'org.apache.grails:grails-gradle-plugins' + implementation 'org.apache.grails.gradle:grails-publish' + implementation "org.asciidoctor.jvm.convert:org.asciidoctor.jvm.convert.gradle.plugin:${gradleProperties.asciidoctorVersion}" +} + diff --git a/build-logic/settings.gradle b/build-logic/settings.gradle new file mode 100644 index 0000000..d712464 --- /dev/null +++ b/build-logic/settings.gradle @@ -0,0 +1,13 @@ +import org.gradle.api.initialization.resolve.RepositoriesMode + +rootProject.name = 'build-logic' + +dependencyResolutionManagement { + repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS + repositories { + if (System.getenv('INCLUDE_MAVEN_LOCAL')) { + mavenLocal() + } + maven { url = 'https://repo.grails.org/grails/restricted' } + } +} diff --git a/build-logic/src/main/groovy/config.app-run.gradle b/build-logic/src/main/groovy/config.app-run.gradle new file mode 100644 index 0000000..8363425 --- /dev/null +++ b/build-logic/src/main/groovy/config.app-run.gradle @@ -0,0 +1,12 @@ +pluginManager.withPlugin('org.springframework.boot') { + tasks.named('bootRun', JavaExec) { + doFirst { + if (project.hasProperty('debugWait')) { + jvmArgs('-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005') + } + if (project.hasProperty('debug')) { + jvmArgs('-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005') + } + } + } +} diff --git a/build-logic/src/main/groovy/config.compile.gradle b/build-logic/src/main/groovy/config.compile.gradle new file mode 100644 index 0000000..457667b --- /dev/null +++ b/build-logic/src/main/groovy/config.compile.gradle @@ -0,0 +1,65 @@ +import java.nio.charset.StandardCharsets + +plugins { + id 'groovy' +} + +tasks.withType(JavaCompile).configureEach { + options.with { + compilerArgs.add('-parameters') + encoding = StandardCharsets.UTF_8.name() + fork = true + incremental = true + release.set(resolveSdkmanJavaMajor(project)) + } + options.forkOptions.with { + jvmArgs.add('-Xmx1g') + memoryMaximumSize = '1g' + } +} + +tasks.withType(GroovyCompile).configureEach { + options.with { + compilerArgs.add('-parameters') + encoding = StandardCharsets.UTF_8.name() + fork = true + incremental = true + } + groovyOptions.with { + encoding = StandardCharsets.UTF_8.name() + optimizationOptions.indy = false + parameters = true + } + groovyOptions.forkOptions.with { + memoryMaximumSize = '1g' + jvmArgs.add('-Xmx1g') + } +} + +private static Provider resolveSdkmanJavaMajor(Project project) { + project.providers.provider { + def sdkmanrc = project.rootProject.file('.sdkmanrc') + if (!sdkmanrc.exists()) { + throw new GradleException('Missing .sdkmanrc in root project') + } + + def props = new Properties() + sdkmanrc.withInputStream { props.load(it) } + + def raw = props.getProperty('java')?.trim() + if (!raw) { + throw new GradleException('Missing java version in root project .sdkmanrc') + } + + def major = raw.tokenize('.').first() + if (!(major ==~ /\d+/)) { + throw new GradleException( + "Invalid java version '$raw' in root project .sdkmanrc (major '$major' is not an integer)" + ) + } + + return major.toInteger() + + } as Provider +} + diff --git a/build-logic/src/main/groovy/config.docs.gradle b/build-logic/src/main/groovy/config.docs.gradle new file mode 100644 index 0000000..712bff8 --- /dev/null +++ b/build-logic/src/main/groovy/config.docs.gradle @@ -0,0 +1,110 @@ +def docProject = provider { + project(":${project.name - 'root'}docs") +} +def pluginProject = provider { + project(":${project.name - '-root'}") +} + +tasks.register('cleanDocs', Delete) { + description = 'Deletes the documentation output' + group = 'documentation' + + delete(rootProject.layout.projectDirectory.dir('build/docs')) +} + +tasks.register('aggregateGroovyApiDoc', Groovydoc) { + description = 'Generates Groovy API Documentation for the plugin project under build/docs/gapi' + group = 'documentation' + + def upstream = pluginProject.flatMap { + it.tasks.named('groovydoc', Groovydoc) + } as Provider + + dependsOn(tasks.named('cleanDocs')) + dependsOn(upstream) + + access = GroovydocAccess.PROTECTED + includeAuthor = false + includeMainForScripts = true + processScripts = true + exclude('**/Application.groovy') + + + source = { upstream.get().source } + destinationDir = rootProject.layout.buildDirectory.dir('docs/gapi').get().asFile + classpath = files({ upstream.get().classpath }) + groovyClasspath = files({ upstream.get().groovyClasspath }) +} + +tasks.register('docs') { + description = 'Generates the documentation' + group = 'documentation' + + dependsOn( + 'aggregateGroovyApiDoc', + docProject.get().tasks.named('asciidoctor') + ) + finalizedBy( + 'copyAsciiDoctorDocs', + 'ghPagesRootIndexPage' + ) +} + +tasks.register('copyAsciiDoctorDocs', Copy) { + group = 'documentation' + + from(docProject.flatMap { it.layout.buildDirectory }) + into(rootProject.layout.buildDirectory) + include('docs/**') + includeEmptyDirs = false + +} + +tasks.register('ghPagesRootIndexPage') { + description = 'Provides a root index page with historical versions fetched from the gh-pages branch at build time' + group = 'documentation' + + def templateFile = docProject.map { it.layout.projectDirectory.file('src/docs/index.tmpl') } + def outputFile = rootProject.layout.buildDirectory.file('docs/ghpages.html') + + inputs.file(templateFile) + outputs.file(outputFile) + + doLast { + def githubUser = rootProject.findProperty('githubUser') as String + def githubProject = rootProject.findProperty('githubProject') as String + + List versions = [] + try { + def conn = URI.create("https://api.github.com/repos/${githubUser}/${githubProject}/contents/?ref=gh-pages").toURL().openConnection() + conn.setRequestProperty('Accept', 'application/vnd.github+json') + conn.setRequestProperty('User-Agent', 'gradle-docs-build') + def parsed = new groovy.json.JsonSlurper().parse(conn.inputStream) + versions = (parsed as List) + .findAll { it.type == 'dir' } + .collect { it.name as String } + .findAll { it ==~ /\d+\.\d+\.(\d+|x)(-.*)?/ } + .sort() + .reverse() + } catch (Exception e) { + logger.warn("ghPagesRootIndexPage: could not fetch GitHub versions — ${e.message}") + } + + String optionsHtml = versions + ? versions.collect { v -> "" }.join('\n ') + : '' + + def tokens = [ + '@OTHER_VERSIONS_OPTIONS@': optionsHtml, + '@GITHUB_REPO_URL@' : "https://github.com/${githubUser}/${githubProject}", + '@GITHUB_ORG_URL@' : "https://github.com/${githubUser}", + '@REPO_SLUG@' : githubProject, + ] + + def out = outputFile.get().asFile + out.parentFile.mkdirs() + def content = templateFile.get().asFile.text + tokens.each { token, value -> content = content.replace(token, value) } + out.text = content + } +} diff --git a/build-logic/src/main/groovy/config.example-app.gradle b/build-logic/src/main/groovy/config.example-app.gradle new file mode 100644 index 0000000..9d5446e --- /dev/null +++ b/build-logic/src/main/groovy/config.example-app.gradle @@ -0,0 +1,8 @@ +plugins { + id 'config.app-run' + id 'config.compile' + id 'config.grails-assets' + id 'config.testing' + id 'org.apache.grails.gradle.grails-web' + id 'org.apache.grails.gradle.grails-gsp' +} diff --git a/build-logic/src/main/groovy/config.grails-assets.gradle b/build-logic/src/main/groovy/config.grails-assets.gradle new file mode 100644 index 0000000..57b9381 --- /dev/null +++ b/build-logic/src/main/groovy/config.grails-assets.gradle @@ -0,0 +1,26 @@ +import asset.pipeline.gradle.AssetPipelineExtension + +plugins { + id 'cloud.wondrify.asset-pipeline' +} + +dependencies { + add('assetDevelopmentRuntime', 'org.webjars.npm:bootstrap') + add('assetDevelopmentRuntime', 'org.webjars.npm:bootstrap-icons') + add('assetDevelopmentRuntime', 'org.webjars.npm:jquery') +} + +extensions.configure(AssetPipelineExtension) { + it.excludes = [ + 'webjars/jquery/**', + 'webjars/bootstrap/**', + 'webjars/bootstrap-icons/**' + ] + it.includes = [ + 'webjars/jquery/*/dist/jquery.js', + 'webjars/bootstrap/*/dist/js/bootstrap.bundle.js', + 'webjars/bootstrap/*/dist/css/bootstrap.css', + 'webjars/bootstrap-icons/*/font/bootstrap-icons.css', + 'webjars/bootstrap-icons/*/font/fonts/*', + ] +} diff --git a/build-logic/src/main/groovy/config.grails-plugin.gradle b/build-logic/src/main/groovy/config.grails-plugin.gradle new file mode 100644 index 0000000..26ddbf4 --- /dev/null +++ b/build-logic/src/main/groovy/config.grails-plugin.gradle @@ -0,0 +1,16 @@ +import asset.pipeline.gradle.AssetPipelineExtension +import org.grails.gradle.plugin.core.GrailsExtension + +plugins { + id 'org.apache.grails.gradle.grails-plugin' + id 'cloud.wondrify.asset-pipeline' +} + +extensions.configure(GrailsExtension) { + // Plugins should avoid the spring dependency management plugin due to how it prefers certain libraries + it.springDependencyManagement = false +} + +extensions.configure(AssetPipelineExtension) { + it.packagePlugin = true +} diff --git a/build-logic/src/main/groovy/config.publish-root.gradle b/build-logic/src/main/groovy/config.publish-root.gradle new file mode 100644 index 0000000..49be5d9 --- /dev/null +++ b/build-logic/src/main/groovy/config.publish-root.gradle @@ -0,0 +1,16 @@ +// Workaround needed for nexus publishing bug +// version and group must be specified in the root project +// https://github.com/gradle-nexus/publish-plugin/issues/310 +version = projectVersion +group = 'io.github.gpc' + +def publishedProjects = projectsToPublish + .tokenize(',') + .collect { it.trim() } + .findAll() + +subprojects { + if (name in publishedProjects) { + apply(plugin: 'org.apache.grails.gradle.grails-publish') + } +} diff --git a/build-logic/src/main/groovy/config.publish.gradle b/build-logic/src/main/groovy/config.publish.gradle new file mode 100644 index 0000000..cf0e078 --- /dev/null +++ b/build-logic/src/main/groovy/config.publish.gradle @@ -0,0 +1,9 @@ +// Useful when testing a release version locally and not wanting to setup signing +pluginManager.withPlugin('signing') { + if (System.getenv('DISABLE_BUILD_SIGNING')) { + logger.lifecycle('Signing is disabled for this build per configuration.') + tasks.withType(Sign).configureEach { + enabled = false + } + } +} diff --git a/build-logic/src/main/groovy/config.testing.gradle b/build-logic/src/main/groovy/config.testing.gradle new file mode 100644 index 0000000..667d35b --- /dev/null +++ b/build-logic/src/main/groovy/config.testing.gradle @@ -0,0 +1,45 @@ +import com.adarshr.gradle.testlogger.TestLoggerExtension + +plugins { + id 'com.adarshr.test-logger' +} + +def isCi = System.getenv('CI') != null +def isWindows = System.getProperty('os.name')?.toLowerCase()?.contains('windows') + +// This configures the 'pretty' test logging +// mocha-parallel uses Unicode symbols that require special config on Windows; +// standard-parallel is a safe fallback there. +extensions.configure(TestLoggerExtension) { + it.theme = isCi ? 'plain-parallel' : (isWindows ? 'standard-parallel' : 'mocha-parallel') + it.showExceptions = true + it.showStandardStreams = false + it.showSummary = true + it.showPassed = true + it.showSkipped = true + it.showFailed = true +} + +tasks.withType(Test).configureEach { + onlyIf { + !project.hasProperty('skipTests') + } + + useJUnitPlatform() + + maxHeapSize = '1g' // set to match the groovy compile task to ensure the worker daemons are reused + + reports { + junitXml.required = false + html.required = true + } + + testLogging { + showStandardStreams = false + exceptionFormat = 'full' + stackTraceFilters = ['groovy'] + events = ['failed', 'skipped', 'standardError'] + showStackTraces = true + showCauses = true + } +} diff --git a/build.gradle b/build.gradle index 402930d..f4e064b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,188 +1,7 @@ -buildscript { - repositories { - mavenLocal() - maven { url "https://repo.grails.org/grails/core" } - } - dependencies { - classpath "org.grails:grails-gradle-plugin:$grailsGradlePluginVersion" - classpath "org.grails.plugins:hibernate5:$gormHibernate5Version" - classpath "io.github.gradle-nexus:publish-plugin:1.0.0" - } +plugins { + id 'idea' + id 'config.docs' + id 'config.publish-root' } -group "io.github.gpc" - -apply plugin:"org.grails.grails-plugin" -apply plugin: "maven-publish" -apply plugin: "signing" -apply plugin: "io.github.gradle-nexus.publish-plugin" - -repositories { - mavenLocal() - maven { url "https://repo.grails.org/grails/core" } -} - -dependencies { - implementation "org.grails:grails-core" - - implementation "org.springframework.boot:spring-boot-starter-logging" - implementation "org.springframework.boot:spring-boot-starter-actuator" - implementation "org.springframework.boot:spring-boot-starter-validation" - implementation "org.springframework.boot:spring-boot-autoconfigure" - implementation "org.springframework.boot:spring-boot-starter-tomcat" - - implementation "org.grails:grails-web-boot" - implementation "org.grails.plugins:hibernate5" - implementation "org.hibernate:hibernate-core" - implementation "org.grails:grails-logging" - implementation "org.grails:grails-plugin-rest" - implementation "org.grails:grails-plugin-databinding" - implementation "org.grails:grails-plugin-i18n" - implementation "org.grails:grails-plugin-services" - implementation "org.grails:grails-plugin-url-mappings" - implementation "org.grails:grails-plugin-interceptors" - implementation "org.grails:grails-datastore-gorm-hibernate5:$gormHibernate5Version" - implementation "org.grails.plugins:cache" - implementation "org.grails.plugins:async" - profile "org.grails.profiles:web-plugin" - runtimeOnly "com.h2database:h2" - runtimeOnly "org.apache.tomcat:tomcat-jdbc" - compileOnly "io.micronaut:micronaut-inject-groovy" - testImplementation "io.micronaut:micronaut-inject-groovy" - testImplementation "org.grails:grails-gorm-testing-support" - testImplementation "org.grails:grails-web-testing-support" - - console "org.grails:grails-console" -} - -jar { - exclude "grails/plugins/taggable/Test*" -} - -publishing { - publications { - maven(MavenPublication) { - groupId = project.group - artifactId = 'grails-taggable-plugin' - version = project.version - - from components.java - artifact sourcesJar - artifact javadocJar - - pom { - name = 'Taggable Grails Plugin' - description = 'The Taggable that adds a generic mechanism for tagging data.' - url = 'https://github.com/gpc/taggable' - licenses { - license { - name = 'The Apache License, Version 2.0' - url = 'https://www.apache.org/licenses/LICENSE-2.0.txt' - } - } - developers { - developer { - id = 'graemerocher' - name = 'Graeme Rocher' - } - developer { - id = 'marcpalmer' - name = 'Marc Palmer' - } - developer { - id = 'pledbrook' - name = 'Peter Ledbrook' - } - developer { - id = 'rhyolight' - name = 'Matthew Taylor ' - } - developer { - id = 'lhotari' - name = 'Lari Hotari' - } - developer { - id = 'codeconsole' - name = 'Scott Murphy' - } - developer { - id = 'jjelliott' - name = 'JJ' - } - developer { - id = 'cazacugmihai' - name = 'Mihai Cazacu' - } - developer { - id = 'stokito' - name = 'Sergey Ponomarev' - } - developer { - id = 'jeffbrown' - name = 'Jeff Scott Brown' - } - developer { - id = 'sbglasius' - name = 'Søren Berg Glasius' - } - } - scm { - connection = 'scm:git:git://github.com/gpc/taggable.git' - developerConnection = 'scm:git:ssh://github.com:gpc/taggable.git' - url = 'https://github.com/gpc/taggable' - } - } - } - } -} - -ext."signing.keyId" = project.findProperty('signing.keyId') ?: System.getenv('SIGNING_KEY_ID') -ext."signing.password" = project.findProperty('signing.password') ?: System.getenv('SIGNING_PASSPHRASE') -ext."signing.secretKeyRingFile" = project.findProperty('signing.secretKeyRingFile') ?: (System.getenv('SIGNING_PASSPHRASE') ?: "${System.getProperty('user.home')}/.gnupg/secring.gpg") - -ext.isReleaseVersion = !version.endsWith("SNAPSHOT") - -afterEvaluate { - signing { - required { isReleaseVersion } - sign publishing.publications.maven - } -} - -tasks.withType(Sign) { - onlyIf { isReleaseVersion } -} - -nexusPublishing { - repositories { - sonatype { - def ossUser = System.getenv("SONATYPE_USERNAME") ?: project.findProperty('sonatypeOss2Username') ?: '' - def ossPass = System.getenv("SONATYPE_PASSWORD") ?: project.findProperty("sonatypeOss2Password") ?: '' - def ossStagingProfileId = System.getenv("SONATYPE_STAGING_PROFILE_ID") ?: project.findProperty("sonatypeOssStagingProfileIdJms") ?: '' - - nexusUrl = uri("https://s01.oss.sonatype.org/service/local/") - snapshotRepositoryUrl = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/") - username = ossUser - password = ossPass - stagingProfileId = ossStagingProfileId - } - } -} - -task snapshotVersion { - doLast { - if (!project.version.endsWith('-SNAPSHOT')) { - ant.propertyfile(file: "gradle.properties") { - entry(key: "version", value: "${project.version}-SNAPSHOT") - } - } - } -} - -tasks.withType(Test) { - useJUnitPlatform() - testLogging { - events = ["passed", "failed", "skipped"] - showStandardStreams = false - } -} +// Intentionally left blank - use composition instead diff --git a/docs/build.gradle b/docs/build.gradle new file mode 100644 index 0000000..efdbab7 --- /dev/null +++ b/docs/build.gradle @@ -0,0 +1,41 @@ +import org.asciidoctor.gradle.jvm.AsciidoctorTask + +plugins { + id 'org.asciidoctor.jvm.convert' +} + +version = projectVersion +group = 'io.github.gpc' + +def asciidoctorAttributes = [ + 'source-highlighter': 'coderay', + toc : 'left', + toclevels : '2', + 'toc-title' : 'Table of Contents', + icons : 'font', + id : "${project.name - '-docs'}:${project.version}", + idprefix : '', + idseparator : '-', + version : project.version, + projectUrl : "https://github.com/${rootProject.findProperty('githubUser')}/${rootProject.findProperty('githubProject')}", + sourcedir : "${rootProject.allprojects.find { it.name == 'taggable' }.projectDir}/src/main/groovy", + grailsVersion : grailsVersion +] + +tasks.named('asciidoctor', AsciidoctorTask) { + + outputDir = project.layout.buildDirectory.dir('docs') + sourceDir = project.layout.projectDirectory.dir('src/docs') + + attributes(asciidoctorAttributes) + baseDirFollowsSourceDir() + options(doctype: 'book') + sources { include 'index.adoc' } + + jvm { + jvmArgs( + '--add-opens', 'java.base/java.io=ALL-UNNAMED', + '--add-opens', 'java.base/sun.nio.ch=ALL-UNNAMED' + ) + } +} diff --git a/docs/src/docs/gettingStarted.adoc b/docs/src/docs/gettingStarted.adoc new file mode 100644 index 0000000..a31a642 --- /dev/null +++ b/docs/src/docs/gettingStarted.adoc @@ -0,0 +1,52 @@ +[[gettingStarted]] +== Getting Started + +=== Requirements + +* Grails {grailsVersion} +* Java 17+ + +=== Installation + +Add the plugin to your application's `build.gradle`: + +[source,groovy,subs="attributes"] +---- +dependencies { + implementation 'io.github.gpc:taggable:{version}' +} +---- + +=== Case sensitivity + +By default, all tags are forced to lower case. Set the following in `application.yml` to preserve the supplied case: + +[source,yaml] +---- +grails: + taggable: + preserve: + case: true +---- + +Finder methods continue to treat each tag as a discrete string, so `"grails"` and `"Grails"` are independent tags when +case preservation is on. + +=== Custom table names + +[source,groovy] +---- +grails.taggable.tag.table = 'MY_TAGS' +grails.taggable.tagLink.table = 'MY_TAG_LINKS' +---- + +=== Hibernate autoImport + +Auto-import is disabled by default for `Tag` / `TagLink` to avoid conflicts with consumer-domain classes of the same +name. Re-enable if required: + +[source,groovy] +---- +grails.taggable.tag.autoImport = true +grails.taggable.tagLink.autoImport = true +---- diff --git a/docs/src/docs/index.adoc b/docs/src/docs/index.adoc new file mode 100644 index 0000000..60f8e2a --- /dev/null +++ b/docs/src/docs/index.adoc @@ -0,0 +1,8 @@ += Taggable Plugin - Reference Documentation +Grails Plugin Collective + +include::introduction.adoc[] + +include::gettingStarted.adoc[] + +include::usage.adoc[] diff --git a/docs/src/docs/index.tmpl b/docs/src/docs/index.tmpl new file mode 100644 index 0000000..a13d6a0 --- /dev/null +++ b/docs/src/docs/index.tmpl @@ -0,0 +1,389 @@ + + + + + + Export - Grails Plugin + + + + + + +
                      + + + + + View on GitHub + +
                      +

                      Export

                      +

                      A Grails plugin that adds export functionality for CSV, Excel, ODS, PDF, RTF, and XML formats

                      +
                      +
                      + +
                      +

                      Documentation

                      + +
                      +
                      + Latest Release +

                      Stable Version

                      + +
                      + +
                      + Snapshot +

                      Development Version

                      + +
                      +
                      + +

                      Other Versions

                      +
                      +
                      + + +
                      +
                      +
                      + + + + + + diff --git a/docs/src/docs/introduction.adoc b/docs/src/docs/introduction.adoc new file mode 100644 index 0000000..99090bb --- /dev/null +++ b/docs/src/docs/introduction.adoc @@ -0,0 +1,12 @@ +[[introduction]] +== Introduction + +The Taggable plugin adds a generic mechanism for tagging any Grails domain class. It supports method chaining, +polymorphic tag queries across class hierarchies, configurable case sensitivity, and a GSP tag library for rendering +tag clouds. + +include::introduction/sourceCode.adoc[] + +include::introduction/currentVersion.adoc[] + +include::introduction/license.adoc[] diff --git a/docs/src/docs/introduction/currentVersion.adoc b/docs/src/docs/introduction/currentVersion.adoc new file mode 100644 index 0000000..a35dc9d --- /dev/null +++ b/docs/src/docs/introduction/currentVersion.adoc @@ -0,0 +1,14 @@ +[[currentVersion]] +=== Current version + +Current version is *{revnumber}*. + +|=== +| Plugin release | Supported Grails Version +| 7.x | >= 7.0 +| 6.x | 6.x +| 5.x | 5.x +| 4.x | 4.x +|=== + +This version is developed against *Grails {grailsVersion}*. diff --git a/docs/src/docs/introduction/license.adoc b/docs/src/docs/introduction/license.adoc new file mode 100644 index 0000000..2d2a9af --- /dev/null +++ b/docs/src/docs/introduction/license.adoc @@ -0,0 +1,6 @@ +[[license]] +=== License + +This plugin is released under the http://www.apache.org/licenses/LICENSE-2.0[Apache License, Version 2.0]. + +See https://github.com/gpc/taggable/blob/master/LICENSE.txt for the full licence text. diff --git a/docs/src/docs/introduction/sourceCode.adoc b/docs/src/docs/introduction/sourceCode.adoc new file mode 100644 index 0000000..a7529ac --- /dev/null +++ b/docs/src/docs/introduction/sourceCode.adoc @@ -0,0 +1,12 @@ +[[sourceCode]] +=== Source code + +The full source code for this plugin can be found on https://github.com/gpc/grails-export[GitHub]. + +The master branch is for the Grails 3.x version. The code for the Grails 2.x version is on branch: `grails-2.x`. + +For issues, improvements or new features go to the plugin's go to https://github.com/gpc/grails-export/issues[GitHub issues]. + +To contact me directly my email address is puneet DOT behl007 AT gmail DOT com + +Feel free to send me any correction about this document. \ No newline at end of file diff --git a/docs/src/docs/usage.adoc b/docs/src/docs/usage.adoc new file mode 100644 index 0000000..48451e3 --- /dev/null +++ b/docs/src/docs/usage.adoc @@ -0,0 +1,93 @@ +[[usage]] +== Usage + +=== Making a domain class taggable + +[source,groovy] +---- +import grails.plugins.taggable.* + +class Vehicle implements Taggable { } +---- + +=== Adding, removing, and replacing tags + +[source,groovy] +---- +def v = Vehicle.get(1) + +v.addTag('red') + .addTag('sporty') + .addTag('expensive') + +v.addTags(['electric', 'hybrid']) + +v.setTags(['red', 'sporty', 'expensive']) + +v.removeTag('expensive') + +def tags = v.tags // ['red', 'sporty'] +---- + +=== Querying by tag + +[source,groovy] +---- +def vehicles = Vehicle.findAllByTag('sporty') // also accepts [max: 5] etc. +def count = Vehicle.countByTag('sporty') +def totalTags = Vehicle.totalTags +def allTags = Vehicle.allTags + +def teslaElectricCars = Vehicle.findAllByTagWithCriteria('electric') { + eq('manufacturer', 'Tesla Motors') +} + +def fiveCoolTags = Vehicle.findAllTagsWithCriteria([max: 5]) { + ilike('name', '%cool%') +} +---- + +=== Querying by tag with HQL + +`Tag` and `TagLink` are mapped GORM entities; you can use them in HQL: + +[source,groovy] +---- +def gasGuzzlers = Vehicle.executeQuery(''' + SELECT vehicle + FROM Vehicle vehicle, TagLink tagLink + WHERE vehicle.id = tagLink.tagRef + AND tagLink.type = 'Vehicle' + AND tagLink.tag.name IN (:tags) +''', [tags: ['SUV', 'gas-guzzler', 'tank', 'boat']]) +---- + +=== Parsing delimited tag strings + +[source,groovy] +---- +v.parseTags('red,sporty,expensive') +v.parseTags('red/sporty/expensive', '/') +---- + +=== Tag cloud taglib + +[source,html] +---- + +---- + +Override the default CSS size classes via configuration: + +[source,yaml] +---- +grails: + taggable: + css: + classes: + - tiny + - small + - medium + - large + - huge +---- diff --git a/examples/app1/build.gradle b/examples/app1/build.gradle new file mode 100644 index 0000000..3e6a918 --- /dev/null +++ b/examples/app1/build.gradle @@ -0,0 +1,47 @@ +plugins { + id 'config.example-app' +} + +version = projectVersion +group = 'app1' + +dependencies { + implementation project(':taggable') + profile "org.apache.grails.profiles:web" + developmentOnly "org.springframework.boot:spring-boot-devtools" + // Spring Boot DevTools may cause performance slowdowns or compatibility issues on larger applications + testAndDevelopmentOnly "org.webjars.npm:bootstrap" + testAndDevelopmentOnly "org.webjars.npm:bootstrap-icons" + testAndDevelopmentOnly "org.webjars.npm:jquery" + implementation platform("org.apache.grails:grails-bom:$grailsVersion") + implementation "org.apache.grails:grails-core" + implementation "org.apache.grails:grails-data-hibernate5" + implementation "org.apache.grails:grails-databinding" + implementation "org.apache.grails:grails-events" + implementation "org.apache.grails:grails-gsp" + implementation "org.apache.grails:grails-interceptors" + implementation "org.apache.grails:grails-layout" + implementation "org.apache.grails:grails-logging" + implementation "org.apache.grails:grails-rest-transforms" + implementation "org.apache.grails:grails-scaffolding" + implementation "org.apache.grails:grails-services" + implementation "org.apache.grails:grails-url-mappings" + implementation "org.apache.grails:grails-web-boot" + implementation "org.springframework.boot:spring-boot-autoconfigure" + implementation "org.springframework.boot:spring-boot-starter" + implementation "org.springframework.boot:spring-boot-starter-actuator" + implementation "org.springframework.boot:spring-boot-starter-logging" + implementation "org.springframework.boot:spring-boot-starter-tomcat" + implementation "org.springframework.boot:spring-boot-starter-validation" + console "org.apache.grails:grails-console" + runtimeOnly "cloud.wondrify:asset-pipeline-grails" + runtimeOnly "com.h2database:h2" + runtimeOnly "com.zaxxer:HikariCP" + runtimeOnly "org.fusesource.jansi:jansi" + integrationTestImplementation testFixtures("org.apache.grails:grails-geb") + testImplementation "org.apache.grails:grails-testing-support-datamapping" + testImplementation "org.apache.grails:grails-testing-support-web" + testImplementation "org.mockito:mockito-core" + testImplementation "org.spockframework:spock-core" +} + diff --git a/examples/app1/grails-app/assets/stylesheets/application.css b/examples/app1/grails-app/assets/stylesheets/application.css new file mode 100644 index 0000000..c1b6dbb --- /dev/null +++ b/examples/app1/grails-app/assets/stylesheets/application.css @@ -0,0 +1,5 @@ +/* + * This is a manifest file that'll be compiled into application.css. + * + *= require_self + */ diff --git a/examples/app1/grails-app/conf/application.yml b/examples/app1/grails-app/conf/application.yml new file mode 100644 index 0000000..86ab46f --- /dev/null +++ b/examples/app1/grails-app/conf/application.yml @@ -0,0 +1,93 @@ +--- +grails: + profile: web + codegen: + defaultPackage: grails.plugins.taggable +info: + app: + name: '@info.app.name@' + version: '@info.app.version@' + grailsVersion: '@info.app.grailsVersion@' +spring: + groovy: + template: + check-template-location: false +server: + port: 8081 + +--- +grails: + mime: + disable: + accept: + header: + userAgents: + - Gecko + - WebKit + - Presto + - Trident + types: + all: '*/*' + atom: application/atom+xml + css: text/css + csv: text/csv + form: application/x-www-form-urlencoded + html: + - text/html + - application/xhtml+xml + js: text/javascript + json: + - application/json + - text/json + multipartForm: multipart/form-data + rss: application/rss+xml + text: text/plain + hal: + - application/hal+json + - application/hal+xml + xml: + - text/xml + - application/xml + urlmapping: + cache: + maxsize: 1000 + controllers: + defaultScope: singleton + converters: + encoding: UTF-8 + views: + default: + codec: html + gsp: + encoding: UTF-8 + htmlcodec: xml + codecs: + expression: html + scriptlets: html + taglib: none + staticparts: none +--- +dataSource: + driverClassName: org.h2.Driver + username: sa + password: '' + pooled: true + jmxExport: true +environments: + development: + dataSource: + dbCreate: create-drop + url: jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE + test: + dataSource: + dbCreate: create-drop + url: jdbc:h2:mem:testDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE + production: + dataSource: + dbCreate: none + url: jdbc:h2:./prodDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE +hibernate: + cache: + queries: false + use_second_level_cache: false + use_query_cache: false diff --git a/grails-app/conf/logback.xml b/examples/app1/grails-app/conf/logback.xml similarity index 100% rename from grails-app/conf/logback.xml rename to examples/app1/grails-app/conf/logback.xml diff --git a/grails-app/domain/grails/plugins/taggable/TestDescendent.groovy b/examples/app1/grails-app/domain/grails/plugins/taggable/TestDescendent.groovy similarity index 100% rename from grails-app/domain/grails/plugins/taggable/TestDescendent.groovy rename to examples/app1/grails-app/domain/grails/plugins/taggable/TestDescendent.groovy diff --git a/grails-app/domain/grails/plugins/taggable/TestDomain.groovy b/examples/app1/grails-app/domain/grails/plugins/taggable/TestDomain.groovy similarity index 100% rename from grails-app/domain/grails/plugins/taggable/TestDomain.groovy rename to examples/app1/grails-app/domain/grails/plugins/taggable/TestDomain.groovy diff --git a/grails-app/init/grails/plugins/taggable/Application.groovy b/examples/app1/grails-app/init/grails/plugins/taggable/Application.groovy similarity index 80% rename from grails-app/init/grails/plugins/taggable/Application.groovy rename to examples/app1/grails-app/init/grails/plugins/taggable/Application.groovy index 09408b0..435540a 100644 --- a/grails-app/init/grails/plugins/taggable/Application.groovy +++ b/examples/app1/grails-app/init/grails/plugins/taggable/Application.groovy @@ -1,13 +1,10 @@ package grails.plugins.taggable - import grails.boot.GrailsApp import grails.boot.config.GrailsAutoConfiguration -import grails.plugins.metadata.PluginSource -@PluginSource class Application extends GrailsAutoConfiguration { static void main(String[] args) { GrailsApp.run(Application, args) } -} \ No newline at end of file +} diff --git a/examples/app1/grails-app/views/layouts/main.gsp b/examples/app1/grails-app/views/layouts/main.gsp new file mode 100644 index 0000000..137125a --- /dev/null +++ b/examples/app1/grails-app/views/layouts/main.gsp @@ -0,0 +1,13 @@ + + + + + + <g:layoutTitle default="Taggable"/> + + + + + + + diff --git a/examples/app1/src/integration-test/groovy/grails/plugins/taggable/TaggableSpec.groovy b/examples/app1/src/integration-test/groovy/grails/plugins/taggable/TaggableSpec.groovy new file mode 100644 index 0000000..4fa065a --- /dev/null +++ b/examples/app1/src/integration-test/groovy/grails/plugins/taggable/TaggableSpec.groovy @@ -0,0 +1,283 @@ +package grails.plugins.taggable + +import spock.lang.Specification + +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration + +@Integration +@Rollback +class TaggableSpec extends Specification { + + void setup() { + Tag.preserveCaseForTesting = false + } + + void testAddTagMethodCaseInsensitive() { + given: + def td = new TestDomain(name: "foo") + td.save() + + when: + td.addTag("Groovy") + .addTag("grails") + and: + def links = TagLink.findAllWhere(tagRef: td.id, type: 'testDomain') + + then: + links.size() == 2 + links.tag.name == ['groovy', 'grails'] + } + + void testAddTagMethodCasePreserving() { + given: + Tag.preserveCaseForTesting = true + + def td = new TestDomain(name: "foo") + td.save() + + when: + td.addTag("Groovy") + .addTag("grails") + + def links = TagLink.findAllWhere(tagRef: td.id, type: 'testDomain') + then: + links.size() == 2 + links.tag.name == ['Groovy', 'grails'] + + when: + // adding a second, even if preserving case in DB it should still not add it as already has such a tag + td.addTag("groovy") + and: + links = TagLink.findAllWhere(tagRef: td.id, type: 'testDomain') + + then: + links.size() == 3 + links.tag.name == ['Groovy', 'grails', 'groovy'] + } + + void testAddTagsMethod() { + given: + def td = new TestDomain(name: "foo") + td.save() + + when: + td.addTags(["groovy", "grails"]) + and: + def links = TagLink.findAllWhere(tagRef: td.id, type: 'testDomain') + + then: + links.size() == 2 + links.tag.name == ['groovy', 'grails'] + } + + void testRemoveTagMethod() { + given: + def td = new TestDomain(name: "foo") + td.save() + + when: + td.addTag("groovy") + .addTag("grails") + and: + def links = TagLink.findAllWhere(tagRef: td.id, type: 'testDomain') + + then: + links.size() == 2 + links.tag.name == ['groovy', 'grails'] + + when: + td.removeTag("groovy") + and: + links = TagLink.findAllWhere(tagRef: td.id, type: 'testDomain') + + then: + links.size() == 1 + links.tag.name == ['grails'] + } + + void testGetTagsMethod() { + given: + def td = new TestDomain(name: "foo") + td.save() + + when: + td.addTag("groovy") + .addTag("grails") + td.save(flush: true) + + and: + TestDomain.withSession { session -> session.clear() } + td = TestDomain.findByName("foo") + + then: + td.tags == ['groovy', 'grails'] + } + + void testSetTagsMethod() { + given: + def td = new TestDomain(name: "foo") + td.save() + + when: + td.tags = ["groovy", null, "grails", ''] + and: + def links = TagLink.findAllWhere(tagRef: td.id, type: 'testDomain') + + then: + links.size() == 2 + links.tag.name == ['groovy', 'grails'] + td.tags == ['groovy', 'grails'] + + when: + td.tags = ["foo", "bar"] + and: + links = TagLink.findAllWhere(tagRef: td.id, type: 'testDomain') + + then: + links.size() == 2 + links.tag.name.sort(true) == ['foo', 'bar'].sort(true) + td.tags.sort(true) == ['foo', 'bar'].sort(true) + + when: + td.tags = [] + and: + links = TagLink.findAllWhere(tagRef: td.id, type: 'testDomain') + + then: + links.size() == 0 + links.tag.name == [] + td.tags == [] + } + + void testFindAllByTag() { + given: + new TestDomain(name: "foo") + .save() + .addTag("groovy") + .addTag("grails") + .addTag("griffon") + new TestDomain(name: "bar") + .save() + .addTag("groovy") + .addTag("grails") + + when: + def results = TestDomain.findAllByTag("groovy") + + then: + results.size() == 2 + results[0] instanceof TestDomain + TestDomain.findAllByTag("groovy").size() == 2 + TestDomain.findAllByTag("grails").size() == 2 + TestDomain.findAllByTag("griffon").size() == 1 + TestDomain.findAllByTag("nothing").size() == 0 + TestDomain.findAllByTag(null).size() == 0 + } + + void testFindAllByTagPolymorphic() { + given: + new TestDomain(name: "foo") + .save() + .addTag("groovy") + .addTag("grails") + .addTag("griffon") + new TestDescendent(name: "bar", other: 'bla') + .save() + .addTag("groovy") + .addTag("grails") + .addTag("gradle") + + when: + def results = TestDomain.findAllByTag("groovy") + + then: + results.size() == 2 + results[0] instanceof TestDomain + TestDomain.findAllByTag("groovy").size() == 2 + TestDescendent.findAllByTag("groovy").size() == 1 + TestDomain.findAllByTag("grails").size() == 2 + TestDescendent.findAllByTag("grails").size() == 1 + TestDomain.findAllByTag("gradle").size() == 1 + TestDescendent.findAllByTag("gradle").size() == 1 + TestDomain.findAllByTag("griffon").size() == 1 + TestDomain.findAllByTag("nothing").size() == 0 + TestDomain.findAllByTag(null).size() == 0 + } + + void testCountByTag() { + given: + new TestDomain(name: "foo") + .save() + .addTag("groovy") + .addTag("grails") + .addTag("griffon") + new TestDomain(name: "bar") + .save() + .addTag("groovy") + .addTag("grails") + new TestDescendent(name: "bla", other: 'zzzz') + .save() + .addTag("groovy") + .addTag("grails") + .addTag("gradle") + + expect: + TestDomain.countByTag("groovy") == 3 + TestDescendent.countByTag("groovy") == 1 + TestDomain.countByTag("griffon") == 1 + TestDescendent.countByTag("griffon") == 0 + TestDomain.countByTag("gradle") == 1 + TestDescendent.countByTag("gradle") == 1 + TestDomain.countByTag("rubbish") == 0 + TestDomain.countByTag(null) == 0 + } + + void testAllTags() { + given: + new TestDomain(name: "foo") + .save() + .addTag("groovy") + .addTag("grails") + .addTag("griffon") + new TestDomain(name: "bar") + .save() + .addTag("groovy") + .addTag("grails") + new TestDescendent(name: "bla", other: 'zzzz') + .save() + .addTag("groovy") + .addTag("grails") + .addTag("gradle") + + expect: + TestDomain.allTags.sort(true) == ['gradle', 'grails', 'griffon', 'groovy'].sort(true) + TestDomain.totalTags == 4 + TestDescendent.allTags.sort(true) == ['gradle', 'grails', 'groovy'].sort(true) + TestDescendent.totalTags == 3 + } + + void testParseTags() { + given: + def td = new TestDomain(name: "foo") + .save() + + when: + td.parseTags("groovy,grails,griffon") + + then: + TestDomain.allTags == ['grails', 'griffon', 'groovy'] + } + + void testParseTagsWithDelimiter() { + given: + def td = new TestDomain(name: "foo") + .save() + + when: + td.parseTags("groovy grails griffon", " ") + + then: + TestDomain.allTags == ['grails', 'griffon', 'groovy'] + } +} diff --git a/gradle.properties b/gradle.properties index 30982f7..371a5a2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,9 +1,18 @@ -version=6.0.0-SNAPSHOT -grailsVersion=6.1.0 -gorm.version=8.0.2 -gormHibernate5Version=8.0.1 -grailsGradlePluginVersion=6.1.0 -groovyVersion=3.0.11 +projectVersion=7.0.0-SNAPSHOT +grailsVersion=7.0.10 +githubUser=gpc +githubProject=taggable +# Comma-separated list of projects to publish +projectsToPublish=taggable +# Build dependencies +asciidoctorVersion=4.0.5 +testLoggerVersion=4.0.0 +# Enable and set agree=yes to publish build scans from GitHub workflows +ciBuildScanPublish=true +ciBuildScanTermsOfUseUrl=https://gradle.com/terms-of-service +ciBuildScanTermsOfUseAgree=yes + +org.gradle.caching=true org.gradle.daemon=true org.gradle.parallel=true -org.gradle.jvmargs=-Dfile.encoding=UTF-8 -Xmx1024M \ No newline at end of file +org.gradle.jvmargs=-Dfile.encoding=UTF-8 -Xmx1024M diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index deedc7fa5e6310eac3148a7dd0b1f069b07364cb..1b33c55baabb587c669f562ae36f953de2481846 100644 GIT binary patch literal 43764 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≺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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d2d5d93..aaaabb3 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip diff --git a/gradlew b/gradlew index 9d82f78..23d15a9 100755 --- a/gradlew +++ b/gradlew @@ -1,74 +1,129 @@ -#!/usr/bin/env bash +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum -warn ( ) { +warn () { echo "$*" -} +} >&2 -die ( ) { +die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null +CLASSPATH="\\\"\\\"" -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -77,84 +132,120 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index aec9973..5eed7ee 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,4 +1,22 @@ -@if "%DEBUG%" == "" @echo off +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -8,26 +26,30 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -35,54 +57,36 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/grails-app/conf/application.yml b/grails-app/conf/application.yml deleted file mode 100644 index 61b5bf3..0000000 --- a/grails-app/conf/application.yml +++ /dev/null @@ -1,110 +0,0 @@ ---- -grails: - profile: web-plugin - codegen: - defaultPackage: grails.plugins.taggable -info: - app: - name: '@info.app.name@' - version: '@info.app.version@' - grailsVersion: '@info.app.grailsVersion@' -spring: - groovy: - template: - check-template-location: false - ---- -grails: - mime: - disable: - accept: - header: - userAgents: - - Gecko - - WebKit - - Presto - - Trident - types: - all: '*/*' - atom: application/atom+xml - css: text/css - csv: text/csv - form: application/x-www-form-urlencoded - html: - - text/html - - application/xhtml+xml - js: text/javascript - json: - - application/json - - text/json - multipartForm: multipart/form-data - rss: application/rss+xml - text: text/plain - hal: - - application/hal+json - - application/hal+xml - xml: - - text/xml - - application/xml - urlmapping: - cache: - maxsize: 1000 - controllers: - defaultScope: singleton - converters: - encoding: UTF-8 - views: - default: - codec: html - gsp: - encoding: UTF-8 - htmlcodec: xml - codecs: - expression: html - scriptlets: html - taglib: none - staticparts: none ---- -hibernate: - cache: - queries: false - use_second_level_cache: false - use_query_cache: false -dataSource: - pooled: true - jmxExport: true - driverClassName: org.h2.Driver - username: sa - password: - -environments: - development: - dataSource: - dbCreate: create-drop - url: jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE - test: - dataSource: - dbCreate: update - url: jdbc:h2:mem:testDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE - production: - dataSource: - dbCreate: update - url: jdbc:h2:prodDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE - properties: - jmxEnabled: true - initialSize: 5 - maxActive: 50 - minIdle: 5 - maxIdle: 25 - maxWait: 10000 - maxAge: 600000 - timeBetweenEvictionRunsMillis: 5000 - minEvictableIdleTimeMillis: 60000 - validationQuery: SELECT 1 - validationQueryTimeout: 3 - validationInterval: 15000 - testOnBorrow: true - testWhileIdle: true - testOnReturn: false - jdbcInterceptors: ConnectionState - defaultTransactionIsolation: 2 # TRANSACTION_READ_COMMITTED diff --git a/plugin/build.gradle b/plugin/build.gradle new file mode 100644 index 0000000..e8cdee9 --- /dev/null +++ b/plugin/build.gradle @@ -0,0 +1,51 @@ +plugins { + id 'config.compile' + id 'config.grails-plugin' + id 'config.publish' + id 'config.testing' +} + +version = projectVersion +group = 'io.github.gpc' + +dependencies { + profile 'org.apache.grails.profiles:web-plugin' + console 'org.apache.grails:grails-console' + + compileOnly platform("org.apache.grails:grails-bom:$grailsVersion") + compileOnly 'org.apache.grails:grails-dependencies-starter-web' + compileOnly 'org.apache.grails.data:grails-datastore-core' + + testImplementation platform("org.apache.grails:grails-bom:$grailsVersion") + testImplementation 'org.apache.grails:grails-dependencies-starter-web' + testImplementation 'org.apache.grails:grails-dependencies-test' + testImplementation 'org.apache.grails:grails-testing-support-web' +} + +extensions.configure(org.apache.grails.gradle.publish.GrailsPublishExtension) { + def githubUser = rootProject.findProperty('githubUser') + def githubProject = rootProject.findProperty('githubProject') + it.artifactId = project.name + it.githubSlug = "${githubUser}/${githubProject}" + it.license.name = 'Apache-2.0' + it.title = 'Grails Taggable Plugin' + it.desc = 'Adds a generic mechanism for tagging any Grails domain class, with tag-cloud taglib support.' + it.organization { + it.name = 'Grails Plugin Collective (GPC)' + it.url = "https://github.com/${githubUser}" + } + it.developers = [ + graemerocher : 'Graeme Rocher', + pledbrook : 'Peter Ledbrook', + marcpalmer : 'Marc Palmer', + jjelliott : 'jjelliott', + codeconsole : 'Scott Murphy', + sbglasius : 'Søren Berg Glasius', + lhotari : 'Lari Hotari', + rhyolight : 'Matthew Taylor', + eusorov : 'Evgeny Usorov', + cazacugmihai : 'Mihai Cazacu', + jeffscottbrown: 'Jeff Scott Brown', + stokito : 'Sergey Ponomarev', + ] +} diff --git a/plugin/grails-app/assets/images/.gitkeep b/plugin/grails-app/assets/images/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/plugin/grails-app/conf/application.yml b/plugin/grails-app/conf/application.yml new file mode 100644 index 0000000..f9adc84 --- /dev/null +++ b/plugin/grails-app/conf/application.yml @@ -0,0 +1,10 @@ +--- +grails: + profile: web-plugin + codegen: + defaultPackage: grails.plugins.taggable +info: + app: + name: '@info.app.name@' + version: '@info.app.version@' + grailsVersion: '@info.app.grailsVersion@' diff --git a/grails-app/domain/grails/plugins/taggable/Tag.groovy b/plugin/grails-app/domain/grails/plugins/taggable/Tag.groovy similarity index 100% rename from grails-app/domain/grails/plugins/taggable/Tag.groovy rename to plugin/grails-app/domain/grails/plugins/taggable/Tag.groovy diff --git a/grails-app/domain/grails/plugins/taggable/TagLink.groovy b/plugin/grails-app/domain/grails/plugins/taggable/TagLink.groovy similarity index 100% rename from grails-app/domain/grails/plugins/taggable/TagLink.groovy rename to plugin/grails-app/domain/grails/plugins/taggable/TagLink.groovy diff --git a/grails-app/services/grails/plugins/taggable/TaggableService.groovy b/plugin/grails-app/services/grails/plugins/taggable/TaggableService.groovy similarity index 51% rename from grails-app/services/grails/plugins/taggable/TaggableService.groovy rename to plugin/grails-app/services/grails/plugins/taggable/TaggableService.groovy index dbedd7a..52f024e 100644 --- a/grails-app/services/grails/plugins/taggable/TaggableService.groovy +++ b/plugin/grails-app/services/grails/plugins/taggable/TaggableService.groovy @@ -1,7 +1,6 @@ package grails.plugins.taggable import grails.core.GrailsApplication -import grails.core.GrailsDomainClass import grails.util.GrailsNameUtils import org.grails.datastore.mapping.model.MappingContext import org.grails.datastore.mapping.model.PersistentEntity @@ -13,7 +12,7 @@ class TaggableService { GrailsApplication grailsApplication @Autowired - @Qualifier("grailsDomainClassMappingContext") + @Qualifier('grailsDomainClassMappingContext') MappingContext mappingContext def domainClassFamilies = [:] @@ -33,30 +32,21 @@ class TaggableService { } return tagCounts } - + /** - * Update the graph of known subclasses - * - * Example: - * [ - * WcmContent: [ - * WcmBlog, - * WcmHTMLContent, - * WcmComment - * ] - * WcmBlog: [], - * WcmHTMLContent: [WcmRichContent], - * WcmRichContent: [], - * WcmStatus: [] - * ] + * Update the graph of known subclasses for each Taggable domain type. */ def refreshDomainClasses() { - grailsApplication.domainClasses.each {GrailsDomainClass artefact -> - PersistentEntity persistentEntity = mappingContext.getPersistentEntity(artefact.clazz.name) - if (Taggable.class.isAssignableFrom(artefact.clazz)) { - domainClassFamilies[artefact.clazz.name] = [GrailsNameUtils.getPropertyName(artefact.clazz)] - // Add class and all subclasses - domainClassFamilies[artefact.clazz.name].addAll(mappingContext.getChildEntities(persistentEntity).collect {GrailsNameUtils.getPropertyName(it.javaClass)}) + mappingContext.persistentEntities.each { PersistentEntity persistentEntity -> + Class clazz = persistentEntity.javaClass + if (Taggable.isAssignableFrom(clazz)) { + def family = [GrailsNameUtils.getPropertyName(clazz)] + family.addAll( + mappingContext.getChildEntities(persistentEntity).collect { + GrailsNameUtils.getPropertyName(it.javaClass) + } + ) + domainClassFamilies[clazz.name] = family } } } diff --git a/grails-app/taglib/grails/plugins/taggable/TagsTagLib.groovy b/plugin/grails-app/taglib/grails/plugins/taggable/TagsTagLib.groovy similarity index 100% rename from grails-app/taglib/grails/plugins/taggable/TagsTagLib.groovy rename to plugin/grails-app/taglib/grails/plugins/taggable/TagsTagLib.groovy diff --git a/src/main/groovy/grails/plugins/taggable/TagException.groovy b/plugin/src/main/groovy/grails/plugins/taggable/TagException.groovy similarity index 100% rename from src/main/groovy/grails/plugins/taggable/TagException.groovy rename to plugin/src/main/groovy/grails/plugins/taggable/TagException.groovy diff --git a/src/main/groovy/grails/plugins/taggable/Taggable.groovy b/plugin/src/main/groovy/grails/plugins/taggable/Taggable.groovy similarity index 54% rename from src/main/groovy/grails/plugins/taggable/Taggable.groovy rename to plugin/src/main/groovy/grails/plugins/taggable/Taggable.groovy index c4efd6b..6406369 100644 --- a/src/main/groovy/grails/plugins/taggable/Taggable.groovy +++ b/plugin/src/main/groovy/grails/plugins/taggable/Taggable.groovy @@ -14,22 +14,26 @@ */ package grails.plugins.taggable -import grails.util.* + +import grails.util.GrailsNameUtils +import grails.util.Holders +import org.grails.datastore.mapping.query.api.BuildableCriteria + /** * Marker interface to add tagging capabilities to a particular domain instance * @author Graeme Rocher */ trait Taggable { - Taggable addTag(String name) { - if(this.id == null) throw new TagException("You need to save the domain instance before tagging it") + Taggable addTag(String name) { + if (this.id == null) throw new TagException("You need to save the domain instance before tagging it") def tag if (!Tag.preserveCase) { name = name.toLowerCase() } - tag = Tag.findByName(name, [cache:true]) ?: new Tag(name:name).save() - if(!tag) throw new TagException("Value [$name] is not a valid tag") - + tag = Tag.findByName(name, [cache: true]) ?: new Tag(name: name).save() + if (!tag) throw new TagException("Value [$name] is not a valid tag") + def criteria = TagLink.createCriteria() def instance = this def link = criteria.get { @@ -38,44 +42,44 @@ trait Taggable { criteria.eq 'type', GrailsNameUtils.getPropertyName(instance.class) criteria.cache true } - - if(!link) { - link = new TagLink(tag:tag, tagRef:this.id, type:GrailsNameUtils.getPropertyName(this.class)).save() + + if (!link) { + link = new TagLink(tag: tag, tagRef: this.id, type: GrailsNameUtils.getPropertyName(this.class)).save() } return this // for method chaining - } + } - - Taggable addTags( names ) { + + Taggable addTags(names) { names.each { addTag it.toString() } return this } - + Collection tags() { - getTags() + getTags() } def getTags() { - this.id ? getTagLinks(Holders.applicationContext.taggableService, this).tag.name : [] - } + this.id ? getTagLinks(this).tag.name : [] + } - Taggable parseTags( String tags, String delimiter = "," ) { - tags.split(delimiter).each { + Taggable parseTags(String tags, String delimiter = ",") { + tags.split(delimiter).each { def tag = it.trim() - if(tag) addTag(tag) + if (tag) addTag(tag) } return this } - Taggable removeTag( String name ) { - if(this.id == null) throw new TagException("You need to save the domain instance before tagging it") - + Taggable removeTag(String name) { + if (this.id == null) throw new TagException("You need to save the domain instance before tagging it") + if (!Tag.preserveCase) { name = name.toLowerCase() } - + def criteria = TagLink.createCriteria() - def instance =this + def instance = this def link = criteria.get { criteria.tag { if (!Tag.preserveCase) { @@ -87,42 +91,40 @@ trait Taggable { criteria.eq 'tagRef', instance.id criteria.eq 'type', GrailsNameUtils.getPropertyName(instance.class) criteria.cache true - } - link?.delete(flush:true) - return this + } + link?.delete(flush: true) + return this } - Taggable setTags( List tags ) { + Taggable setTags(List tags) { // remove invalid tags - tags = tags?.findAll { it } + tags = tags?.findAll { it } if (tags) { // remove old tags that not appear in the new tags - getTagLinks(Holders.applicationContext.taggableService, this)*.each { TagLink tagLink -> + getTagLinks(this)*.each { TagLink tagLink -> if (tags.contains(tagLink.tag.name)) { tags.remove(tagLink.tag.name) } else { - tagLink.delete(flush:true) // Grails >=3.3.0 requires flush + tagLink.delete(flush: true) // Grails >=3.3.0 requires flush } } // add the rest addTags(tags) } else { - getTagLinks(Holders.applicationContext.taggableService, this)*.delete(flush:true) + getTagLinks(this)*.delete(flush: true) } return this } - - static List getAllTags() { - def criteria = TagLink.createCriteria() + def criteria = TagLink.createCriteria() criteria.list { criteria.projections { criteria.tag { criteria.distinct "name" } } - criteria.'in'('type', (Collection)Holders.applicationContext.taggableService.domainClassFamilies[this.name]) + criteria.'in'('type', (Collection) Holders.applicationContext.getBean(TaggableService).domainClassFamilies[this.name]) criteria.cache true - } + } as List } static Integer getTotalTags() { @@ -130,13 +132,14 @@ trait Taggable { def criteria = TagLink.createCriteria() criteria.get { criteria.projections { criteria.tag { criteria.countDistinct "name" } } - criteria.'in'('type', (Collection)Holders.applicationContext.taggableService.domainClassFamilies[clazz.name]) + criteria.'in'('type', (Collection) Holders.applicationContext.getBean(TaggableService).domainClassFamilies[clazz.name]) criteria.cache true - } + } as Integer } - static Integer countByTag( String tag ) { - def identifiers = getTagReferences(Holders.applicationContext.taggableService, tag, this.name) - if(identifiers) { + + static Integer countByTag(String tag) { + def identifiers = getTagReferences(tag, this.name) + if (identifiers) { def criteria = createCriteria() criteria.get { criteria.projections { @@ -145,56 +148,53 @@ trait Taggable { criteria.inList 'id', identifiers criteria.cache true } - } - else { - return 0 + } else { + return 0 } } - - static List findAllByTag( String name ) { - def identifiers = getTagReferences(Holders.applicationContext.taggableService, name, this.name) - if(identifiers) { - return findAllByIdInList(identifiers, [cache:true]) - } - else { - return Collections.EMPTY_LIST + + static List findAllByTag(String name) { + def identifiers = getTagReferences(name, this.name) + if (identifiers) { + return findAllByIdInList(identifiers, [cache: true]) + } else { + return Collections.EMPTY_LIST } } - static List findAllByTag ( String name, Map args ) { - def identifiers = getTagReferences(Holders.applicationContext.taggableService, name, this.name) - if(identifiers) { - args.cache=true + static List findAllByTag(String name, Map args) { + def identifiers = getTagReferences(name, this.name) + if (identifiers) { + args.cache = true return findAllByIdInList(identifiers, args) - } - else { - return Collections.EMPTY_LIST + } else { + return Collections.EMPTY_LIST } } - static List findAllByTagWithCriteria( String name, Closure crit ) { + static List findAllByTagWithCriteria(String name, Closure crit) { def clazz = this - def identifiers = getTagReferences(Holders.applicationContext.taggableService, name, clazz.name) - if(identifiers) { + def identifiers = getTagReferences(name, clazz.name) + if (identifiers) { return clazz.withCriteria { 'in'('id', identifiers) crit.delegate = delegate crit.call() } - } - else { + } else { return Collections.EMPTY_LIST } } - static List findAllTagsWithCriteria( Map params, Closure crit ) { + + static List findAllTagsWithCriteria(Map params, Closure crit) { def clazz = this - def criteria = TagLink.createCriteria() + BuildableCriteria criteria = TagLink.createCriteria() criteria.list { - criteria.projections { criteria.tag { criteria.distinct "name" } } - criteria.'in'('type', Holders.applicationContext.taggableService.domainClassFamilies[clazz.name]) - criteria.cache true - criteria.tag(crit) + criteria.projections { criteria.tag { criteria.distinct "name" } } + criteria.'in'('type', Holders.applicationContext.getBean(TaggableService).domainClassFamilies[clazz.name]) + criteria.cache true + criteria.tag(crit) if (params.offset != null) { criteria.firstResult(params.offset.toInteger()) @@ -205,31 +205,33 @@ trait Taggable { criteria.tag { criteria.order('name', 'asc') } - } + } as List } - private getTagLinks(tagService, obj) { - TagLink.findAllByTagRefAndTypeInList(obj.id, tagService.domainClassFamilies[obj.class.name], [cache:true]) + + private getTagLinks(obj) { + TaggableService tagService = Holders.applicationContext.getBean(TaggableService) + TagLink.findAllByTagRefAndTypeInList(obj.id, tagService.domainClassFamilies[obj.class.name], [cache: true]) } - private static getTagReferences(tagService, String tagName, String className) { - if(tagName) { - def criteria = TagLink.createCriteria() + private static getTagReferences(String tagName, String className) { + TaggableService tagService = Holders.applicationContext.getBean(TaggableService) + if (tagName) { + def criteria = TagLink.createCriteria() criteria.list { criteria.projections { criteria.property 'tagRef' } criteria.tag { - criteria.eq 'name', tagName - } + criteria.eq 'name', tagName + } criteria.'in'('type', tagService.domainClassFamilies[className]) criteria.cache true } - - } - else { + + } else { return Collections.EMPTY_LIST - } - } + } + } -} \ No newline at end of file +} diff --git a/src/main/groovy/grails/plugins/taggable/TaggableGrailsPlugin.groovy b/plugin/src/main/groovy/grails/plugins/taggable/TaggableGrailsPlugin.groovy similarity index 81% rename from src/main/groovy/grails/plugins/taggable/TaggableGrailsPlugin.groovy rename to plugin/src/main/groovy/grails/plugins/taggable/TaggableGrailsPlugin.groovy index bbb6677..91693dd 100644 --- a/src/main/groovy/grails/plugins/taggable/TaggableGrailsPlugin.groovy +++ b/plugin/src/main/groovy/grails/plugins/taggable/TaggableGrailsPlugin.groovy @@ -24,13 +24,9 @@ import grails.plugins.* * @author Graeme Rocher */ class TaggableGrailsPlugin extends Plugin { - def version = "2.1.0" - def grailsVersion = "3.3.0 > *" + + def grailsVersion = "7.0.0 > *" def license = 'APACHE' - def pluginExcludes = [ - "grails-app/views/error.gsp", - "grails/plugins/taggable/Test*" - ] def observe = ['hibernate'] def developers = [ @@ -42,10 +38,10 @@ A plugin that adds a generic mechanism for tagging data. ''' // URL to the plugin's documentation - def documentation = "http://grails.org/plugin/taggable" - def issueManagement = [system: "JIRA", url: "http://jira.grails.org/browse/GPTAGGABLE"] + def documentation = "https://gpc.github.io/taggable" + def issueManagement = [system: "JIRA", url: "https://github.com/gpc/grails-taggable/issues"] def scm = [url: "https://github.com/gpc/grails-taggable"] - def organization = [ name: "Grails Plugin Collective", url: "http://github.com/gpc" ] + def organization = [ name: "Grails Plugin Collective", url: "https://github.com/gpc" ] void doWithApplicationContext() { def tagService = applicationContext.taggableService diff --git a/src/test/groovy/grails/plugins/taggable/TagsTagLibSpec.groovy b/plugin/src/test/groovy/grails/plugins/taggable/TagsTagLibSpec.groovy similarity index 61% rename from src/test/groovy/grails/plugins/taggable/TagsTagLibSpec.groovy rename to plugin/src/test/groovy/grails/plugins/taggable/TagsTagLibSpec.groovy index a648ec7..10f3ba5 100644 --- a/src/test/groovy/grails/plugins/taggable/TagsTagLibSpec.groovy +++ b/plugin/src/test/groovy/grails/plugins/taggable/TagsTagLibSpec.groovy @@ -6,34 +6,32 @@ import spock.lang.Specification class TagsTagLibSpec extends Specification implements TagLibUnitTest { - def setup() { + void setup() { webRequest.controllerName = 'default' config.setAt("grails.taggable.css.classes", null) } void "test that tagCloud fails when no action attribute specified"() { when: - tagLib.tagCloud tags: [:] + applyTemplate('', [tags: [:]]) then: Exception e = thrown(GrailsTagException) - e != null - e.getMessage() == "Required attribute [action] is missing" + e.getMessage().endsWith( "Required attribute [action] is missing") } void "test that tagCloud fails when no tags attribute specified"() { when: - tagLib.tagCloud action: "browseByTag" + applyTemplate('', [action: "browseByTag"]) then: Exception e = thrown(GrailsTagException) - e != null - e.getMessage() == "Required attribute [tags] must be a map of tag names to tag counts" + e.getMessage().endsWith("Required attribute [tags] must be a map of tag names to tag counts") } void "test that tagCloud uses current controller when no attr passed"() { when: - def result = tagLib.tagCloud(tags: [hello: 1, world: 10, apple: 3, orange: 7], action: "byTag") + def result = applyTemplate('', [tags: [hello: 1, world: 10, apple: 3, orange: 7], action: "byTag"]) then: - result.toString() == "
                      1. " + + result == "
                        1. " + "hello
                        2. " + "
                        3. world
                        4. " + "
                        5. apple
                        6. " + @@ -43,9 +41,9 @@ class TagsTagLibSpec extends Specification implements TagLibUnitTest void "test tagCloud with long counts"() { when: - def result = tagLib.tagCloud(tags: [hello: 1L, world: 10L, apple: 3L, orange: 7L], action: "byTag") + def result = applyTemplate('', [tags: [hello: 1L, world: 10L, apple: 3L, orange: 7L], action: "byTag"]) then: - result.toString() == "
                          1. " + + result == "
                            1. " + "hello
                            2. " + "
                            3. world
                            4. " + "
                            5. apple
                            6. " + @@ -55,9 +53,9 @@ class TagsTagLibSpec extends Specification implements TagLibUnitTest void "test tagCloud with controller attribute"() { when: - def result = tagLib.tagCloud(tags: [hello: 1, world: 10, apple: 3, orange: 7], controller: "plugin", action: "byTag") + def result = applyTemplate('', [tags: [hello: 1, world: 10, apple: 3, orange: 7], controller: "plugin", action: "byTag"]) then: - result.toString() == "
                              1. " + + result == "
                                1. " + "hello
                                2. " + "
                                3. world
                                4. " + "
                                5. apple
                                6. " + @@ -67,9 +65,9 @@ class TagsTagLibSpec extends Specification implements TagLibUnitTest void "test tagCloud with custom id property"() { when: - def result = tagLib.tagCloud(tags: [hello: 1, world: 10, apple: 3, orange: 7], controller: "plugin", action: "byTag", idProperty: "tagName") + def result = applyTemplate('', [tags: [hello: 1, world: 10, apple: 3, orange: 7], controller: "plugin", action: "byTag", idProperty: "tagName"]) then: - result.toString() == "
                                  1. " + + result == "
                                    1. " + "hello
                                    2. " + "
                                    3. world
                                    4. " + "
                                    5. apple
                                    6. " + @@ -86,9 +84,9 @@ class TagsTagLibSpec extends Specification implements TagLibUnitTest "four" ] when: - def result = tagLib.tagCloud(tags: [hello: 2, world: 10, apple: 3, orange: 7], controller: "plugin", action: "byTag") + def result = applyTemplate('', [tags: [hello: 2, world: 10, apple: 3, orange: 7], controller: "plugin", action: "byTag"]) then: - result.toString() == "
                                      1. " + + result == "
                                        1. " + "hello
                                        2. " + "
                                        3. world
                                        4. " + "
                                        5. apple
                                        6. " + diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..895b1d8 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,57 @@ +import org.gradle.api.initialization.resolve.RepositoriesMode + +pluginManagement { + repositories { + if (System.getenv('INCLUDE_MAVEN_LOCAL')) { + mavenLocal() + } + maven { url = 'https://repo.grails.org/grails/restricted' } + } + + includeBuild('./build-logic') { + it.name = 'build-logic' + } +} + +plugins { + id 'com.gradle.common-custom-user-data-gradle-plugin' version '2.3' +} + +def isCI = System.getenv().containsKey('CI') +def isLocal = !isCI +def isReproducibleBuild = System.getenv('SOURCE_DATE_EPOCH') != null +if (isReproducibleBuild) { + gradle.settingsEvaluated { + logger.warn( + '***** Remote Build Cache Disabled due to Reproducible Build *****\n' + + 'Build date will be set to (SOURCE_DATE_EPOCH={})', + System.getenv('SOURCE_DATE_EPOCH') + ) + } +} + +buildCache { + local { enabled = (isLocal && !isReproducibleBuild) || (isCI && isReproducibleBuild) } +} + +rootProject.name = 'taggable-root' + +include('plugin') +project(':plugin').name = 'taggable' +include('docs') +project(':docs').name = 'taggable-docs' + +file('examples').listFiles({ it.directory } as FileFilter).each { + include(it.name) + project(":$it.name").projectDir = file("examples/$it.name") +} + +dependencyResolutionManagement { + repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS + repositories { + if (System.getenv('INCLUDE_MAVEN_LOCAL')) { + mavenLocal() + } + maven { url = 'https://repo.grails.org/grails/restricted' } + } +} diff --git a/src/integration-test/groovy/grails/plugins/taggable/TaggableSpec.groovy b/src/integration-test/groovy/grails/plugins/taggable/TaggableSpec.groovy deleted file mode 100644 index 7f6d737..0000000 --- a/src/integration-test/groovy/grails/plugins/taggable/TaggableSpec.groovy +++ /dev/null @@ -1,298 +0,0 @@ -package grails.plugins.taggable - -import grails.gorm.transactions.Rollback -import grails.testing.mixin.integration.Integration -import spock.lang.Specification - -import static org.junit.Assert.assertEquals -import static org.junit.Assert.assertTrue - -@Integration -@Rollback -class TaggableSpec extends Specification { - - def setup() { - Tag.preserveCaseForTesting = false - } - - void testAddTagMethodCaseInsensitive() { - given: - def td = new TestDomain(name: "foo") - td.save() - - when: - td.addTag("Groovy") - .addTag("grails") - and: - def links = TagLink.findAllWhere(tagRef: td.id, type: 'testDomain') - - then: - assertEquals 2, links.size() - assertEquals(['groovy', 'grails'], links.tag.name) - } - - void testAddTagMethodCasePreserving() { - given: - Tag.preserveCaseForTesting = true - - def td = new TestDomain(name:"foo") - td.save() - - when: - td.addTag("Groovy") - .addTag("grails") - - def links = TagLink.findAllWhere(tagRef:td.id, type:'testDomain') - then: - assertEquals 2, links.size() - assertEquals( ['Groovy', 'grails'], links.tag.name ) - - when: - // adding a second, even if preserving case in DB it should still not add it as already has such a tag - td.addTag("groovy") - and: - links = TagLink.findAllWhere(tagRef:td.id, type:'testDomain') - - then: - assertEquals 3, links.size() - assertEquals( ['Groovy', 'grails', 'groovy'], links.tag.name ) - } - - void testAddTagsMethod() { - given: - def td = new TestDomain(name:"foo") - td.save() - - when: - td.addTags(["groovy","grails"]) - and: - def links = TagLink.findAllWhere(tagRef:td.id, type:'testDomain') - - then: - assertEquals 2, links.size() - assertEquals( ['groovy', 'grails'], links.tag.name ) - } - - void testRemoveTagMethod() { - given: - def td = new TestDomain(name: "foo") - td.save() - - when: - td.addTag("groovy") - .addTag("grails") - and: - def links = TagLink.findAllWhere(tagRef: td.id, type: 'testDomain') - - then: - assertEquals 2, links.size() - assertEquals(['groovy', 'grails'], links.tag.name) - - when: - td.removeTag("groovy") - and: - links = TagLink.findAllWhere(tagRef: td.id, type: 'testDomain') - - then: - assertEquals 1, links.size() - assertEquals(['grails'], links.tag.name) - } - - void testGetTagsMethod() { - given: - def td = new TestDomain(name:"foo") - td.save() - - when: - td.addTag("groovy") - .addTag("grails") - td.save(flush:true) - - and: - TestDomain.withSession { session -> session.clear() } - td = TestDomain.findByName("foo") - - then: - assertEquals( ['groovy', 'grails'], td.tags ) - } - - void testSetTagsMethod() { - given: - def td = new TestDomain(name:"foo") - td.save() - - when: - td.tags = ["groovy", null, "grails", ''] - and: - def links = TagLink.findAllWhere(tagRef:td.id, type:'testDomain') - - then: - assertEquals 2, links.size() - assertEquals( ['groovy', 'grails'], links.tag.name ) - assertEquals( ['groovy', 'grails'], td.tags ) - - when: - td.tags = ["foo", "bar"] - and: - links = TagLink.findAllWhere(tagRef:td.id, type:'testDomain') - - then: - assertEquals 2, links.size() - assertEquals( ['foo', 'bar'].sort(true), links.tag.name.sort(true) ) - assertEquals( ['foo', 'bar'].sort(true), td.tags.sort(true) ) - - when: - td.tags = [] - and: - links = TagLink.findAllWhere(tagRef:td.id, type:'testDomain') - - then: - assertEquals 0, links.size() - assertEquals( [], links.tag.name ) - assertEquals( [], td.tags ) - } - - void testFindAllByTag() { - given: - new TestDomain(name:"foo") - .save() - .addTag("groovy") - .addTag("grails") - .addTag("griffon") - new TestDomain(name:"bar") - .save() - .addTag("groovy") - .addTag("grails") - - when: - def results = TestDomain.findAllByTag("groovy") - - then: - assertEquals 2, results.size() - assertTrue results[0] instanceof TestDomain - - assertEquals 2, TestDomain.findAllByTag("groovy").size() - assertEquals 2, TestDomain.findAllByTag("grails").size() - assertEquals 1, TestDomain.findAllByTag("griffon").size() - assertEquals 0, TestDomain.findAllByTag("nothing").size() - assertEquals 0, TestDomain.findAllByTag(null).size() - - } - - void testFindAllByTagPolymorphic() { - given: - new TestDomain(name:"foo") - .save() - .addTag("groovy") - .addTag("grails") - .addTag("griffon") - new TestDescendent(name:"bar", other:'bla') - .save() - .addTag("groovy") - .addTag("grails") - .addTag("gradle") - - when: - def results = TestDomain.findAllByTag("groovy") - - then: - assertEquals 2, results.size() - assertTrue results[0] instanceof TestDomain - - assertEquals 2, TestDomain.findAllByTag("groovy").size() - assertEquals 1, TestDescendent.findAllByTag("groovy").size() - - assertEquals 2, TestDomain.findAllByTag("grails").size() - assertEquals 1, TestDescendent.findAllByTag("grails").size() - - assertEquals 1, TestDomain.findAllByTag("gradle").size() - assertEquals 1, TestDescendent.findAllByTag("gradle").size() - - assertEquals 1, TestDomain.findAllByTag("griffon").size() - assertEquals 0, TestDomain.findAllByTag("nothing").size() - assertEquals 0, TestDomain.findAllByTag(null).size() - - } - - void testCountByTag() { - given: - new TestDomain(name:"foo") - .save() - .addTag("groovy") - .addTag("grails") - .addTag("griffon") - new TestDomain(name:"bar") - .save() - .addTag("groovy") - .addTag("grails") - new TestDescendent(name:"bla", other:'zzzz') - .save() - .addTag("groovy") - .addTag("grails") - .addTag("gradle") - - expect: - assertEquals 3, TestDomain.countByTag("groovy") - assertEquals 1, TestDescendent.countByTag("groovy") - - assertEquals 1, TestDomain.countByTag("griffon") - assertEquals 0, TestDescendent.countByTag("griffon") - - assertEquals 1, TestDomain.countByTag("gradle") - assertEquals 1, TestDescendent.countByTag("gradle") - - assertEquals 0, TestDomain.countByTag("rubbish") - assertEquals 0, TestDomain.countByTag(null) - - } - - void testAllTags() { - given: - new TestDomain(name:"foo") - .save() - .addTag("groovy") - .addTag("grails") - .addTag("griffon") - new TestDomain(name:"bar") - .save() - .addTag("groovy") - .addTag("grails") - new TestDescendent(name:"bla", other:'zzzz') - .save() - .addTag("groovy") - .addTag("grails") - .addTag("gradle") - - expect: - assertEquals( ['gradle','grails','griffon','groovy'].sort(true), TestDomain.allTags.sort(true) ) - assertEquals 4, TestDomain.totalTags - - assertEquals( ['gradle','grails','groovy'].sort(true), TestDescendent.allTags.sort(true) ) - assertEquals 3, TestDescendent.totalTags - } - - void testParseTags() { - given: - def td = new TestDomain(name:"foo") - .save() - - when: - td.parseTags("groovy,grails,griffon") - - then: - assertEquals( ['grails','griffon','groovy'], TestDomain.allTags ) - } - - void testParseTagsWithDelimiter() { - given: - def td = new TestDomain(name:"foo") - .save() - - when: - td.parseTags("groovy grails griffon", " ") - - then: - assertEquals( ['grails','griffon','groovy'], TestDomain.allTags ) - - } -} diff --git a/travis-build.sh b/travis-build.sh deleted file mode 100755 index abe9f5d..0000000 --- a/travis-build.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -set -e -rm -rf *.zip -./gradlew clean test assemble - -filename=$(find build/libs -name "*.jar" | head -1) -filename=$(basename "$filename") - -EXIT_STATUS=0 -echo "Publishing archives for branch $TRAVIS_BRANCH" -if [[ -n $TRAVIS_TAG ]] || [[ $TRAVIS_BRANCH == 'master' && $TRAVIS_PULL_REQUEST == 'false' ]]; then - - echo "Publishing archives" - - if [[ -n $TRAVIS_TAG ]]; then - ./gradlew bintrayUpload || EXIT_STATUS=$? - else - ./gradlew publish || EXIT_STATUS=$? - fi - -fi -exit $EXIT_STATUS \ No newline at end of file From 299bf6ab383760e928dcb9d6d9b09949e56020a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Berg=20Glasius?= Date: Wed, 22 Apr 2026 16:56:44 +0200 Subject: [PATCH 3/6] Update docs/src/docs/index.tmpl Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/src/docs/index.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/docs/index.tmpl b/docs/src/docs/index.tmpl index a13d6a0..2d366d3 100644 --- a/docs/src/docs/index.tmpl +++ b/docs/src/docs/index.tmpl @@ -3,7 +3,7 @@ - Export - Grails Plugin + Taggable - Grails Plugin From 113e870ca9222010147c47d1036520bb154b8e1a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 15:04:23 +0000 Subject: [PATCH 4/6] docs: update source code doc links to gpc/taggable Agent-Logs-Url: https://github.com/gpc/taggable/sessions/59110057-0ccf-440a-8d17-5e0f63499671 Co-authored-by: sbglasius <625868+sbglasius@users.noreply.github.com> --- docs/src/docs/introduction/sourceCode.adoc | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/docs/src/docs/introduction/sourceCode.adoc b/docs/src/docs/introduction/sourceCode.adoc index a7529ac..5d7e860 100644 --- a/docs/src/docs/introduction/sourceCode.adoc +++ b/docs/src/docs/introduction/sourceCode.adoc @@ -1,12 +1,8 @@ [[sourceCode]] === Source code -The full source code for this plugin can be found on https://github.com/gpc/grails-export[GitHub]. +The full source code for this plugin can be found on https://github.com/gpc/taggable[GitHub]. -The master branch is for the Grails 3.x version. The code for the Grails 2.x version is on branch: `grails-2.x`. +For issues, improvements, or new features, go to https://github.com/gpc/taggable/issues[GitHub issues]. -For issues, improvements or new features go to the plugin's go to https://github.com/gpc/grails-export/issues[GitHub issues]. - -To contact me directly my email address is puneet DOT behl007 AT gmail DOT com - -Feel free to send me any correction about this document. \ No newline at end of file +Feel free to send me any correction about this document. From ed85273f97a294689092384e972e93e0fbea1553 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 15:12:11 +0000 Subject: [PATCH 5/6] docs: make license link branch agnostic Agent-Logs-Url: https://github.com/gpc/taggable/sessions/06ae9e5a-0ce2-4dc4-b0fb-9db4723bf799 Co-authored-by: sbglasius <625868+sbglasius@users.noreply.github.com> --- docs/src/docs/introduction/license.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/docs/introduction/license.adoc b/docs/src/docs/introduction/license.adoc index 2d2a9af..d746171 100644 --- a/docs/src/docs/introduction/license.adoc +++ b/docs/src/docs/introduction/license.adoc @@ -3,4 +3,4 @@ This plugin is released under the http://www.apache.org/licenses/LICENSE-2.0[Apache License, Version 2.0]. -See https://github.com/gpc/taggable/blob/master/LICENSE.txt for the full licence text. +See https://github.com/gpc/taggable/blob/HEAD/LICENSE.txt for the full licence text. From 25d3d811533d85a8201dff7b4a576e6927b4d84d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 15:13:23 +0000 Subject: [PATCH 6/6] docs: normalize license spelling in intro page Agent-Logs-Url: https://github.com/gpc/taggable/sessions/06ae9e5a-0ce2-4dc4-b0fb-9db4723bf799 Co-authored-by: sbglasius <625868+sbglasius@users.noreply.github.com> --- docs/src/docs/introduction/license.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/docs/introduction/license.adoc b/docs/src/docs/introduction/license.adoc index d746171..6645ac0 100644 --- a/docs/src/docs/introduction/license.adoc +++ b/docs/src/docs/introduction/license.adoc @@ -3,4 +3,4 @@ This plugin is released under the http://www.apache.org/licenses/LICENSE-2.0[Apache License, Version 2.0]. -See https://github.com/gpc/taggable/blob/HEAD/LICENSE.txt for the full licence text. +See https://github.com/gpc/taggable/blob/HEAD/LICENSE.txt for the full license text.