۱۳۹۷ اسفند ۷, سه‌شنبه

ایجاد فایل برای دانلود در Spring

یکی از نیازمندی هام این بود که یک متنی رو در قالب فایل برای دانلود، به کاربر ارایه بدم، بعنوان مثال فرض کنیم یه سریال نامبر داریم که کاربر میتونه به صورت فایل متنی دانلود اش کنه. بعد از کلی گشت و گذار به این راه حل رسیدم که کار میکنه:

@GetMapping(path = "/file/get", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public ResponseEntity verificationFile() {
 HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.TEXT_PLAIN);
    headers.setContentDispositionFormData("attachment", "test.txt");
 InputStreamResource inputStreamResource = new InputStreamResource(new ByteArrayInputStream("Hello File".getBytes(StandardCharsets.UTF_8)));
 return ResponseEntity.ok().headers(headers).body(inputStreamResource);
}

بعد از فراخوانی آدرس http://{SERVER-ADDRESS}:{PORT}/file/get فایلی با عنوان test.txt و با محتوای Hello File دانلود میشه.

۱۳۹۷ اسفند ۴, شنبه

محدود کردن جستجوی الستیک روی داده های کاربر جاری در JHipster

وقتی یه پروژه رو با JHipster ایجاد میکنیم، یکی از بخش های مهم که برامون انجام نمیده محدود کردن نمایش داده های هر کاربر برای خود اون کاربر. به صورت پیشفرض وقتی کاربری درخواست نوع داده ای رو میکنه، تمام داده های موجود رو برای کاربر ارسال میکنه (که راه حل برطرف کردن این مشکل با یه سرچ کوچیک پیدا میکنید).
اما وقتی Elasticsearch رو به پروژه اضافه میکنیم و وظیفه جستجو رو به اون واگذار میکنیم، یه مشکل جدید هم ایجاد میشه، اینکه جستجوی کاربر روی تمام داده ای موجود همه کاربران اتفاق میافتد در صورتیکه هر کاربر فقط مجاز به جستجو و مشاهده داده های خودش، نه تمام داده های موجود! خب به صورت پیشفرض نحوه جستجو رو به اینصورت پیاده سازی کرده که تولید مشکل میکند:

return StreamSupport.stream(categorySearchRepository.search(queryStringQuery(query))
    .spliterator(), false).collect(Collectors.toList());

و ما باید یه کار کوچیک روش انجام بدیم و اون رو تبدیل کنیم به این کد:

BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery().must(queryStringQuery(query));
if (SecurityUtils.isAuthenticated() && !SecurityUtils.isCurrentUserInRole(AuthoritiesConstants.ADMIN)) {
 queryBuilder = queryBuilder.filter(matchQuery("user.login", SecurityUtils.getCurrentUserLogin().orElse("")));
}
return StreamSupport.stream(categorySearchRepository.search(queryBuilder).spliterator(), false).collect(Collectors.toList());
تو کد بالا، چک میکنیم اگه کاربر، ادمین نبود (AuthoritiesConstants.ADMIN)، به quryBuilder یک فیلتر اضافه میکنیم که براساس کاربر جاری جستجو انجام بشه.
راه حل این مشکل تو اپلیکیشن نمونه 21-point روی گیت هاب وجود داره که منبع منم همون کد بود (PointsResource.java).

۱۳۹۷ بهمن ۱۸, پنجشنبه

Lombok، Hibernate و خطای 'StackOverflowError: null'!

یکی از کتابخانه های محبوبم در جاوا Project Lombok، کمک بزرگی برای تمیز و خوانا بودن کد، برای اینکه متوجه منظورم بشید فقط یه نگاه به نمونه کد اش برای @Data (اینجا) بندازید و اونوقت شما هم به من حق میدید!
 ولی استفاده از @Data روی یک entity و استفاده از این شی داخل یک entity دیگه (مثلا یک Set از کامنتهای یک پست بلاگ)، شما رو با خطای java.lang.StackOverflowError: null مواجه میکنه! @Data یک میانبری برای این انوتیشن های Lombok:
  • @Getter
  • @Setter
  • @ToString
  • @RequiredArgsConstructor
  • @EqualsAndHashCode
 و همه چی بخاطر @EqualsAndHashCode! من یک راه حل برای این مشکل در آخرین کامنت این سوال StackOverflow پیدا کردم: https://stackoverflow.com/questions/34972895 که میگه باید بعد از @Data از این انوتیشن استفاده کنیم و فیلد های رابطه ای رو از متدهای Equals و Hash حذف کنیم:

@EqualsAndHashCode(exclude="entries")
من تو پروژه Feader ام از این کتابخانه استفاده کردم و میتونید به عنوان یه نمونه روش بررسی هاتون رو انجام بدید. Feed.java و FeedEntry.java کلاسهایی هستند که مورد استفاده قرار گرفتند، FeedEntry داخل Feed استفاده شده (مثل کامنتهای یک پست):

package com.glinboy.feader.model;

import java.util.Set;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.Lob;
import javax.persistence.ManyToMany;
import javax.persistence.OneToMany;

import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;

import lombok.Data;
import lombok.EqualsAndHashCode;

@Entity
@Data
@EqualsAndHashCode(callSuper=true, exclude="entries")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Feed extends BaseModel {

 private static final long serialVersionUID = 7720285113632455831L;
 
 @Column(length=50)
 private String title;
 
 @Column(length=256)
    private String link;
    
    @Lob
    private String description;
 
    @ManyToMany
    @JoinTable(name="feed_category",
      joinColumns = @JoinColumn(name = "feed_id", nullable=false),
            inverseJoinColumns = @JoinColumn(name = "category_id", nullable=false))
    private Set category;

    @OneToMany(mappedBy="feed")
    private Set entries;
}

package com.glinboy.feader.model;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.Lob;
import javax.persistence.ManyToOne;

import lombok.Data;
import lombok.EqualsAndHashCode;

@Entity
@Data
@EqualsAndHashCode(callSuper=true, exclude="feed")
public class FeedEntry extends BaseModel {

 private static final long serialVersionUID = -7086809764656937022L;
 
 @Column(length=128)
 private String title;
 
 @Lob
 private String description;
 
 @Column(length=512)
 private String link;
 
 @Column(length=64)
 private String author;
 
 @Column(length=40)
 private String guid;
 
 @ManyToOne
 @JoinColumn(name="feed_id", nullable=false)
 private Feed feed;
}